Skip to content

Commit 263b12c

Browse files
committed
[feature|optimize|build] Support custom playlists; optimize paging state indication; update dependencies
1 parent 431e5cf commit 263b12c

File tree

89 files changed

+3796
-520
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+3796
-520
lines changed

app/build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ android {
2222
minSdk = 24
2323
targetSdk = 35
2424
versionCode = 26
25-
versionName = "3.1-alpha07"
25+
versionName = "3.1-beta01"
2626

2727
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2828

app/src/androidTest/java/com/skyd/anivu/MediaModule.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ class MediaModule {
141141
})
142142
mediaRepository.changeMediaGroup(
143143
path,
144-
MediaBean(file = file1, articleWithEnclosure = null, feedBean = null),
144+
MediaBean(file = file1, fileCount = 0, articleWithEnclosure = null, feedBean = null),
145145
group
146146
).first()
147147
assertTrue(mediaRepository.requestFiles(path, group).first().first().file == file1)
@@ -250,7 +250,7 @@ class MediaModule {
250250
mediaRepository.moveFilesToGroup(path, MediaGroupBean.DefaultMediaGroup, group).first()
251251
val displayName = ":/*-\\`~"
252252
mediaRepository.setFileDisplayName(
253-
MediaBean(file = file2, articleWithEnclosure = null, feedBean = null),
253+
MediaBean(file = file2, fileCount = 0, articleWithEnclosure = null, feedBean = null),
254254
displayName
255255
).first()
256256
assertNotNull(mediaRepository.requestFiles(path, group).first()

app/src/androidTest/java/com/skyd/anivu/RssModule.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.skyd.anivu.model.db.AppDatabase
1818
import com.skyd.anivu.model.db.dao.ArticleDao
1919
import com.skyd.anivu.model.db.dao.FeedDao
2020
import com.skyd.anivu.model.db.dao.GroupDao
21+
import com.skyd.anivu.model.db.dao.ReadHistoryDao
2122
import com.skyd.anivu.model.repository.ArticleRepository
2223
import com.skyd.anivu.model.repository.ArticleSort
2324
import com.skyd.anivu.model.repository.ReadRepository
@@ -78,6 +79,7 @@ class RssModule {
7879
private lateinit var groupDao: GroupDao
7980
private lateinit var feedDao: FeedDao
8081
private lateinit var articleDao: ArticleDao
82+
private lateinit var readHistoryDao: ReadHistoryDao
8183
private var rssHelper: RssHelper = RssHelper(okHttpClient, faviconExtractor)
8284
private lateinit var reorderGroupRepository: ReorderGroupRepository
8385
private lateinit var feedRepository: FeedRepository
@@ -873,13 +875,14 @@ class RssModule {
873875
groupDao = db.groupDao()
874876
feedDao = db.feedDao()
875877
articleDao = db.articleDao()
878+
readHistoryDao = db.readHistoryDao()
876879

877880
reorderGroupRepository = ReorderGroupRepository(groupDao)
878881
feedRepository =
879882
FeedRepository(groupDao, feedDao, articleDao, reorderGroupRepository, rssHelper)
880883
articleRepository = ArticleRepository(feedDao, articleDao, rssHelper, pagingConfig)
881884
searchRepository = SearchRepository(feedDao, articleDao, pagingConfig)
882-
readRepository = ReadRepository(articleDao)
885+
readRepository = ReadRepository(articleDao, readHistoryDao)
883886
requestHeadersRepository = RequestHeadersRepository(feedDao)
884887
}
885888

app/src/main/java/com/skyd/anivu/di/DatabaseModule.kt

+11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import com.skyd.anivu.model.db.dao.RssModuleDao
1515
import com.skyd.anivu.model.db.dao.SearchDomainDao
1616
import com.skyd.anivu.model.db.dao.SessionParamsDao
1717
import com.skyd.anivu.model.db.dao.TorrentFileDao
18+
import com.skyd.anivu.model.db.dao.playlist.PlaylistDao
19+
import com.skyd.anivu.model.db.dao.playlist.PlaylistMediaDao
1820
import dagger.Module
1921
import dagger.Provides
2022
import dagger.hilt.InstallIn
@@ -78,6 +80,15 @@ object DatabaseModule {
7880
fun provideArticleNotificationRuleDao(database: AppDatabase): ArticleNotificationRuleDao =
7981
database.articleNotificationRuleDao()
8082

83+
@Provides
84+
@Singleton
85+
fun providePlaylistDao(database: AppDatabase): PlaylistDao = database.playlistDao()
86+
87+
88+
@Provides
89+
@Singleton
90+
fun providePlaylistItemDao(database: AppDatabase): PlaylistMediaDao = database.playlistItemDao()
91+
8192
@Provides
8293
@Singleton
8394
fun provideSearchDomainDatabase(@ApplicationContext context: Context): SearchDomainDatabase =

app/src/main/java/com/skyd/anivu/ext/FlowExt.kt

+14
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.lifecycle.LifecycleOwner
55
import androidx.lifecycle.flowWithLifecycle
66
import kotlinx.coroutines.CancellationException
77
import kotlinx.coroutines.CoroutineStart
8+
import kotlinx.coroutines.channels.Channel
89
import kotlinx.coroutines.flow.Flow
910
import kotlinx.coroutines.flow.FlowCollector
1011
import kotlinx.coroutines.flow.buffer
@@ -86,3 +87,16 @@ suspend fun <T> Flow<T>.collectIn(
8687
fun <T> Flow<T>.sampleWithoutFirst(timeoutMillis: Long) = merge(
8788
take(1), drop(1).sample(timeoutMillis)
8889
)
90+
91+
/**
92+
* Like PV operation in semaphore, but we do V first and then P
93+
*/
94+
suspend infix fun Channel<Unit>.vThenP(receiveChannel: Channel<Unit>): Channel<Unit> {
95+
send(Unit)
96+
return receiveChannel
97+
}
98+
99+
suspend infix fun Channel<Unit>.on(block: () -> Unit) {
100+
block()
101+
receive()
102+
}

app/src/main/java/com/skyd/anivu/ext/PaddingValuesExt.kt

+8-1
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,11 @@ operator fun PaddingValues.plus(other: PaddingValues): PaddingValues = PaddingVa
1818
)
1919

2020
@Composable
21-
operator fun PaddingValues.plus(other: Dp): PaddingValues = this + PaddingValues(other)
21+
operator fun PaddingValues.plus(other: Dp): PaddingValues = this + PaddingValues(other)
22+
23+
@Composable
24+
fun PaddingValues.topPaddingRemoved(): PaddingValues = PaddingValues(
25+
bottom = calculateBottomPadding(),
26+
start = calculateStartPadding(LocalLayoutDirection.current),
27+
end = calculateEndPadding(LocalLayoutDirection.current),
28+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.skyd.anivu.ext
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
5+
import androidx.compose.runtime.remember
6+
import androidx.compose.runtime.snapshotFlow
7+
import androidx.paging.LoadState
8+
import androidx.paging.compose.LazyPagingItems
9+
import androidx.paging.compose.itemKey
10+
import kotlinx.coroutines.channels.Channel
11+
import kotlinx.coroutines.flow.distinctUntilChanged
12+
import kotlinx.coroutines.flow.drop
13+
import kotlinx.coroutines.flow.filter
14+
15+
fun <T : Any> LazyPagingItems<T>.safeItemKey(
16+
default: (Int) -> Any = { it },
17+
key: ((item: @JvmSuppressWildcards T) -> Any)? = null,
18+
): (index: Int) -> Any {
19+
return { index ->
20+
if (index >= itemCount) {
21+
default(index)
22+
} else {
23+
itemKey(key).invoke(index)
24+
}
25+
}
26+
}
27+
28+
fun <T : Any> LazyPagingItems<T>.getOrNull(index: Int): T? {
29+
return if (index !in 0..<itemCount) {
30+
null
31+
} else {
32+
get(index)
33+
}
34+
}
35+
36+
@Composable
37+
fun <T : Any, U> LazyPagingItems<T>.rememberUpdateSemaphore(
38+
default: U?,
39+
sendData: suspend (LazyPagingItems<T>) -> U? = { default },
40+
): Channel<U> {
41+
val semaphoreChannel = remember { Channel<U>(capacity = Channel.UNLIMITED) }
42+
LaunchedEffect(Unit) {
43+
snapshotFlow { itemSnapshotList.items }
44+
.distinctUntilChanged()
45+
.filter { loadState.refresh is LoadState.NotLoading }
46+
.drop(1)
47+
.collect {
48+
sendData(this@rememberUpdateSemaphore)?.let {
49+
semaphoreChannel.trySend(it)
50+
}
51+
}
52+
}
53+
return semaphoreChannel
54+
}

app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt

+8-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ import com.skyd.anivu.model.preference.behavior.media.MediaListSortAscPreference
3939
import com.skyd.anivu.model.preference.behavior.media.MediaListSortByPreference
4040
import com.skyd.anivu.model.preference.behavior.media.MediaSubListSortAscPreference
4141
import com.skyd.anivu.model.preference.behavior.media.MediaSubListSortByPreference
42+
import com.skyd.anivu.model.preference.behavior.playlist.PlaylistMediaSortAscPreference
43+
import com.skyd.anivu.model.preference.behavior.playlist.PlaylistMediaSortByPreference
44+
import com.skyd.anivu.model.preference.behavior.playlist.PlaylistSortAscPreference
45+
import com.skyd.anivu.model.preference.behavior.playlist.PlaylistSortByPreference
4246
import com.skyd.anivu.model.preference.data.OpmlExportDirPreference
4347
import com.skyd.anivu.model.preference.data.autodelete.AutoDeleteArticleBeforePreference
4448
import com.skyd.anivu.model.preference.data.autodelete.AutoDeleteArticleFrequencyPreference
@@ -119,7 +123,10 @@ fun Preferences.toSettings(): Settings {
119123
mediaSubListSortAsc = MediaSubListSortAscPreference.fromPreferences(this),
120124
mediaListSortBy = MediaListSortByPreference.fromPreferences(this),
121125
mediaSubListSortBy = MediaSubListSortByPreference.fromPreferences(this),
122-
126+
playlistSortAsc = PlaylistSortAscPreference.fromPreferences(this),
127+
playlistMediaSortAsc = PlaylistMediaSortAscPreference.fromPreferences(this),
128+
playlistSortBy = PlaylistSortByPreference.fromPreferences(this),
129+
playlistMediaSortBy = PlaylistMediaSortByPreference.fromPreferences(this),
123130
// RSS
124131
rssSyncFrequency = RssSyncFrequencyPreference.fromPreferences(this),
125132
rssSyncWifiConstraint = RssSyncWifiConstraintPreference.fromPreferences(this),

app/src/main/java/com/skyd/anivu/model/bean/article/ArticleWithFeed.kt

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,12 @@ data class ArticleWithFeed(
1717
var articleWithEnclosure: ArticleWithEnclosureBean,
1818
@Relation(parentColumn = ArticleBean.FEED_URL_COLUMN, entityColumn = FeedBean.URL_COLUMN)
1919
var feed: FeedBean,
20-
) : Serializable, Parcelable
20+
) : Serializable, Parcelable {
21+
fun getThumbnail(): String? {
22+
return articleWithEnclosure.media?.image ?: feed.customIcon ?: feed.icon
23+
}
24+
25+
fun getArtist(): String? {
26+
return articleWithEnclosure.article.author.orEmpty().ifEmpty { feed.title }
27+
}
28+
}

app/src/main/java/com/skyd/anivu/model/bean/history/MediaPlayHistoryBean.kt

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ data class MediaPlayHistoryBean(
1717
@PrimaryKey
1818
@ColumnInfo(name = PATH_COLUMN)
1919
val path: String,
20+
@ColumnInfo(name = DURATION_COLUMN)
21+
val duration: Long,
2022
@ColumnInfo(name = LAST_PLAY_POSITION_COLUMN)
2123
val lastPlayPosition: Long,
2224
@ColumnInfo(name = LAST_TIME_COLUMN)
@@ -26,6 +28,7 @@ data class MediaPlayHistoryBean(
2628
) : BaseBean, Parcelable {
2729
companion object {
2830
const val PATH_COLUMN = "path"
31+
const val DURATION_COLUMN = "duration"
2932
const val LAST_PLAY_POSITION_COLUMN = "lastPlayPosition"
3033
const val LAST_TIME_COLUMN = "lastTime"
3134
const val ARTICLE_ID_COLUMN = "articleId"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.skyd.anivu.model.bean.playlist
2+
3+
import android.os.Parcelable
4+
import androidx.room.ColumnInfo
5+
import androidx.room.Entity
6+
import androidx.room.PrimaryKey
7+
import com.skyd.anivu.base.BaseBean
8+
import kotlinx.parcelize.Parcelize
9+
import kotlinx.serialization.Serializable
10+
11+
const val PLAYLIST_TABLE_NAME = "Playlist"
12+
13+
@Parcelize
14+
@Serializable
15+
@Entity(tableName = PLAYLIST_TABLE_NAME)
16+
data class PlaylistBean(
17+
@PrimaryKey
18+
@ColumnInfo(name = PLAYLIST_ID_COLUMN)
19+
val playlistId: String,
20+
@ColumnInfo(name = NAME_COLUMN)
21+
val name: String,
22+
@ColumnInfo(name = ORDER_POSITION_COLUMN)
23+
val orderPosition: Double,
24+
@ColumnInfo(name = CREATE_TIME_COLUMN)
25+
val createTime: Long,
26+
@ColumnInfo(name = DELETE_MEDIA_ON_FINISH_COLUMN)
27+
val deleteMediaOnFinish: Boolean,
28+
) : BaseBean, Parcelable {
29+
companion object {
30+
const val PLAYLIST_ID_COLUMN = "playlistId"
31+
const val NAME_COLUMN = "name"
32+
const val ORDER_POSITION_COLUMN = "orderPosition"
33+
const val CREATE_TIME_COLUMN = "createTime"
34+
const val DELETE_MEDIA_ON_FINISH_COLUMN = "deleteMediaOnFinish"
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.skyd.anivu.model.bean.playlist
2+
3+
import android.media.MediaMetadataRetriever
4+
import androidx.room.ColumnInfo
5+
import androidx.room.Entity
6+
import androidx.room.ForeignKey
7+
import androidx.room.Ignore
8+
import com.skyd.anivu.base.BaseBean
9+
import com.skyd.anivu.ext.isLocalFile
10+
import kotlinx.serialization.Serializable
11+
12+
const val PLAYLIST_MEDIA_TABLE_NAME = "PlaylistMedia"
13+
14+
@Serializable
15+
@Entity(
16+
tableName = PLAYLIST_MEDIA_TABLE_NAME,
17+
primaryKeys = [PlaylistMediaBean.PLAYLIST_ID_COLUMN, PlaylistMediaBean.URL_COLUMN],
18+
foreignKeys = [
19+
ForeignKey(
20+
entity = PlaylistBean::class,
21+
parentColumns = [PlaylistBean.PLAYLIST_ID_COLUMN],
22+
childColumns = [PlaylistMediaBean.PLAYLIST_ID_COLUMN],
23+
onDelete = ForeignKey.CASCADE
24+
)
25+
],
26+
)
27+
data class PlaylistMediaBean(
28+
@ColumnInfo(name = PLAYLIST_ID_COLUMN)
29+
val playlistId: String,
30+
@ColumnInfo(name = URL_COLUMN)
31+
val url: String,
32+
@ColumnInfo(name = ARTICLE_ID_COLUMN)
33+
val articleId: String?,
34+
@ColumnInfo(name = ORDER_POSITION_COLUMN)
35+
val orderPosition: Double,
36+
@ColumnInfo(name = CREATE_TIME_COLUMN)
37+
val createTime: Long,
38+
) : BaseBean {
39+
fun isSamePlaylistMedia(other: PlaylistMediaBean?): Boolean {
40+
other ?: return false
41+
return playlistId == other.playlistId && url == other.url
42+
}
43+
44+
@Ignore
45+
val isLocalFile = url.isLocalFile()
46+
47+
@Ignore
48+
var title: String? = null
49+
50+
@Ignore
51+
var duration: Long? = null
52+
53+
@Ignore
54+
var artist: String? = null
55+
56+
@Ignore
57+
var thumbnail: String? = null
58+
59+
fun updateLocalMediaMetadata() {
60+
val retriever = MediaMetadataRetriever()
61+
try {
62+
with(retriever) {
63+
setDataSource(url)
64+
duration =
65+
extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull()
66+
title = extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
67+
artist = extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
68+
}
69+
} catch (e: Exception) {
70+
e.printStackTrace()
71+
}
72+
retriever.release()
73+
}
74+
75+
companion object {
76+
const val PLAYLIST_ID_COLUMN = "playlistId"
77+
const val URL_COLUMN = "url"
78+
const val ARTICLE_ID_COLUMN = "articleId"
79+
const val ORDER_POSITION_COLUMN = "orderPosition"
80+
const val CREATE_TIME_COLUMN = "createTime"
81+
}
82+
}

0 commit comments

Comments
 (0)