diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c6273058..fa9bac49 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,7 +22,7 @@ android { minSdk = 24 targetSdk = 35 versionCode = 24 - versionName = "2.1-alpha31" + versionName = "2.1-beta01" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/com/skyd/anivu/di/DownloadModule.kt b/app/src/main/java/com/skyd/anivu/di/DownloadModule.kt deleted file mode 100644 index 4b61079b..00000000 --- a/app/src/main/java/com/skyd/anivu/di/DownloadModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.skyd.anivu.di - -import com.skyd.anivu.model.db.dao.DownloadInfoDao -import com.skyd.anivu.model.db.dao.SessionParamsDao -import com.skyd.anivu.model.db.dao.TorrentFileDao -import com.skyd.anivu.model.repository.download.DownloadManager -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object DownloadModule { - @Provides - @Singleton - fun provideDownloadManager( - downloadInfoDao: DownloadInfoDao, - sessionParamsDao: SessionParamsDao, - torrentFileDao: TorrentFileDao, - ): DownloadManager = DownloadManager(downloadInfoDao, sessionParamsDao, torrentFileDao) -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ext/FlowExt.kt b/app/src/main/java/com/skyd/anivu/ext/FlowExt.kt index 19dd3ce6..fd8ff93e 100644 --- a/app/src/main/java/com/skyd/anivu/ext/FlowExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/FlowExt.kt @@ -12,12 +12,16 @@ import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.produceIn +import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean @@ -82,3 +86,7 @@ fun Flow.collectIn( ): Job = lifecycleOwner.lifecycleScope.launch { flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState).collect(action) } + +fun Flow.debounceWithoutFirst(timeoutMillis: Long) = merge( + take(1), drop(1).debounce(timeoutMillis) +) diff --git a/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt b/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt index 0e48354b..a48450c1 100644 --- a/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt @@ -14,7 +14,7 @@ import com.skyd.anivu.model.preference.appearance.article.ArticleListTonalElevat import com.skyd.anivu.model.preference.appearance.article.ArticleTopBarTonalElevationPreference import com.skyd.anivu.model.preference.appearance.article.ShowArticlePullRefreshPreference import com.skyd.anivu.model.preference.appearance.article.ShowArticleTopBarRefreshPreference -import com.skyd.anivu.model.preference.appearance.feed.FeedGroupExpandPreference +import com.skyd.anivu.model.preference.appearance.feed.FeedDefaultGroupExpandPreference import com.skyd.anivu.model.preference.appearance.feed.FeedListTonalElevationPreference import com.skyd.anivu.model.preference.appearance.feed.FeedTopBarTonalElevationPreference import com.skyd.anivu.model.preference.appearance.media.MediaShowThumbnailPreference @@ -61,7 +61,7 @@ fun Preferences.toSettings(): Settings { // Appearance theme = ThemePreference.fromPreferences(this), darkMode = DarkModePreference.fromPreferences(this), - feedGroupExpand = FeedGroupExpandPreference.fromPreferences(this), + feedDefaultGroupExpand = FeedDefaultGroupExpandPreference.fromPreferences(this), textFieldStyle = TextFieldStylePreference.fromPreferences(this), dateStyle = DateStylePreference.fromPreferences(this), navigationBarLabel = NavigationBarLabelPreference.fromPreferences(this), diff --git a/app/src/main/java/com/skyd/anivu/model/bean/GroupVo.kt b/app/src/main/java/com/skyd/anivu/model/bean/GroupVo.kt index 190f3aa7..bf93e4d1 100644 --- a/app/src/main/java/com/skyd/anivu/model/bean/GroupVo.kt +++ b/app/src/main/java/com/skyd/anivu/model/bean/GroupVo.kt @@ -6,7 +6,7 @@ import com.skyd.anivu.appContext import com.skyd.anivu.base.BaseBean import com.skyd.anivu.ext.dataStore import com.skyd.anivu.ext.getOrDefault -import com.skyd.anivu.model.preference.appearance.feed.FeedGroupExpandPreference +import com.skyd.anivu.model.preference.appearance.feed.FeedDefaultGroupExpandPreference import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -25,11 +25,13 @@ open class GroupVo( GroupVo( DEFAULT_GROUP_ID, appContext.getString(R.string.default_feed_group), - appContext.dataStore.getOrDefault(FeedGroupExpandPreference), + appContext.dataStore.getOrDefault(FeedDefaultGroupExpandPreference), ) { private fun readResolve(): Any = DefaultGroup override val name: String get() = appContext.getString(R.string.default_feed_group) + override val isExpanded: Boolean + get() = appContext.dataStore.getOrDefault(FeedDefaultGroupExpandPreference) } companion object { diff --git a/app/src/main/java/com/skyd/anivu/model/db/dao/GroupDao.kt b/app/src/main/java/com/skyd/anivu/model/db/dao/GroupDao.kt index 5924ea98..8c6e2aba 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/dao/GroupDao.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/dao/GroupDao.kt @@ -171,6 +171,13 @@ interface GroupDao { } } + @Transaction + @Query( + "UPDATE `$GROUP_TABLE_NAME` SET ${GroupBean.IS_EXPANDED_COLUMN} = :expanded " + + "WHERE ${GroupBean.GROUP_ID_COLUMN} = :groupId" + ) + suspend fun changeGroupExpanded(groupId: String, expanded: Boolean): Int + @Transaction @Query("SELECT * FROM `$GROUP_TABLE_NAME`") fun getGroupWithFeeds(): Flow> diff --git a/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt b/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt index 182c2bcd..ec72e7bf 100644 --- a/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt +++ b/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt @@ -19,7 +19,7 @@ import com.skyd.anivu.model.preference.appearance.article.ArticleListTonalElevat import com.skyd.anivu.model.preference.appearance.article.ArticleTopBarTonalElevationPreference import com.skyd.anivu.model.preference.appearance.article.ShowArticlePullRefreshPreference import com.skyd.anivu.model.preference.appearance.article.ShowArticleTopBarRefreshPreference -import com.skyd.anivu.model.preference.appearance.feed.FeedGroupExpandPreference +import com.skyd.anivu.model.preference.appearance.feed.FeedDefaultGroupExpandPreference import com.skyd.anivu.model.preference.appearance.feed.FeedListTonalElevationPreference import com.skyd.anivu.model.preference.appearance.feed.FeedTopBarTonalElevationPreference import com.skyd.anivu.model.preference.appearance.media.MediaShowThumbnailPreference @@ -72,7 +72,7 @@ import com.skyd.anivu.ui.local.LocalAutoDeleteArticleFrequency import com.skyd.anivu.ui.local.LocalDarkMode import com.skyd.anivu.ui.local.LocalDateStyle import com.skyd.anivu.ui.local.LocalDeduplicateTitleInDesc -import com.skyd.anivu.ui.local.LocalFeedGroupExpand +import com.skyd.anivu.ui.local.LocalFeedDefaultGroupExpand import com.skyd.anivu.ui.local.LocalFeedListTonalElevation import com.skyd.anivu.ui.local.LocalFeedTopBarTonalElevation import com.skyd.anivu.ui.local.LocalHardwareDecode @@ -120,7 +120,7 @@ data class Settings( // Appearance val theme: String = ThemePreference.default, val darkMode: Int = DarkModePreference.default, - val feedGroupExpand: Boolean = FeedGroupExpandPreference.default, + val feedDefaultGroupExpand: Boolean = FeedDefaultGroupExpandPreference.default, val textFieldStyle: String = TextFieldStylePreference.default, val dateStyle: String = DateStylePreference.default, val navigationBarLabel: String = NavigationBarLabelPreference.default, @@ -191,7 +191,7 @@ fun SettingsProvider( // Appearance LocalTheme provides settings.theme, LocalDarkMode provides settings.darkMode, - LocalFeedGroupExpand provides settings.feedGroupExpand, + LocalFeedDefaultGroupExpand provides settings.feedDefaultGroupExpand, LocalTextFieldStyle provides settings.textFieldStyle, LocalDateStyle provides settings.dateStyle, LocalNavigationBarLabel provides settings.navigationBarLabel, diff --git a/app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedGroupExpandPreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedDefaultGroupExpandPreference.kt similarity index 78% rename from app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedGroupExpandPreference.kt rename to app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedDefaultGroupExpandPreference.kt index 77ed5bad..71f9d04d 100644 --- a/app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedGroupExpandPreference.kt +++ b/app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedDefaultGroupExpandPreference.kt @@ -10,11 +10,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -object FeedGroupExpandPreference : BasePreference { - private const val FEED_GROUP_EXPAND = "feedGroupExpand" +object FeedDefaultGroupExpandPreference : BasePreference { + private const val FEED_DEFAULT_GROUP_EXPAND = "feedDefaultGroupExpand" override val default = true - val key = booleanPreferencesKey(FEED_GROUP_EXPAND) + val key = booleanPreferencesKey(FEED_DEFAULT_GROUP_EXPAND) fun put(context: Context, scope: CoroutineScope, value: Boolean) { scope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/com/skyd/anivu/model/repository/download/DownloadManager.kt b/app/src/main/java/com/skyd/anivu/model/repository/download/DownloadManager.kt index 8e068ba8..7f8227ee 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/download/DownloadManager.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/download/DownloadManager.kt @@ -2,6 +2,7 @@ package com.skyd.anivu.model.repository.download import androidx.work.WorkManager import com.skyd.anivu.appContext +import com.skyd.anivu.ext.debounceWithoutFirst import com.skyd.anivu.model.bean.download.DownloadInfoBean import com.skyd.anivu.model.bean.download.DownloadInfoBean.DownloadState import com.skyd.anivu.model.bean.download.DownloadLinkUuidMapBean @@ -10,22 +11,159 @@ import com.skyd.anivu.model.bean.download.TorrentFileBean import com.skyd.anivu.model.db.dao.DownloadInfoDao import com.skyd.anivu.model.db.dao.SessionParamsDao import com.skyd.anivu.model.db.dao.TorrentFileDao +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.UUID -import javax.inject.Inject -class DownloadManager @Inject constructor( - private val downloadInfoDao: DownloadInfoDao, - private val sessionParamsDao: SessionParamsDao, - private val torrentFileDao: TorrentFileDao, -) { +object DownloadManager { + @EntryPoint + @InstallIn(SingletonComponent::class) + interface DownloadManagerPoint { + val downloadInfoDao: DownloadInfoDao + val sessionParamsDao: SessionParamsDao + val torrentFileDao: TorrentFileDao + } + + private val hiltEntryPoint = EntryPointAccessors.fromApplication( + appContext, DownloadManagerPoint::class.java + ) + + private val downloadInfoDao = hiltEntryPoint.downloadInfoDao + private val sessionParamsDao = hiltEntryPoint.sessionParamsDao + private val torrentFileDao = hiltEntryPoint.torrentFileDao + private val scope = CoroutineScope(Dispatchers.IO) + private val intentFlow = MutableSharedFlow() private lateinit var downloadInfoMap: LinkedHashMap private lateinit var downloadInfoListFlow: MutableStateFlow> + init { + intentFlow + .onEachIntent() + .launchIn(scope) + } + + private fun Flow.onEachIntent(): Flow { + return merge( + filterIsInstance() + .debounceWithoutFirst(100) + .onEach { intent -> + downloadInfoDao.updateDownloadInfo(intent.downloadInfoBean) + putDownloadInfoToMap( + link = intent.downloadInfoBean.link, + newInfo = intent.downloadInfoBean, + ) + }.catch { it.printStackTrace() }, + + filterIsInstance() + .onEach { intent -> + sessionParamsDao.updateSessionParams( + SessionParamsBean( + link = intent.link, + data = intent.sessionStateData, + ) + ) + }.catch { it.printStackTrace() }, + + filterIsInstance() + .debounceWithoutFirst(1000) + .onEach { intent -> + val result = downloadInfoDao.updateDownloadProgress( + link = intent.link, + progress = intent.progress, + ) + if (result > 0) { + updateDownloadInfoMap(intent.link) { copy(progress = intent.progress) } + } + }.catch { it.printStackTrace() }, + + filterIsInstance() + .onEach { intent -> + val result = downloadInfoDao.updateDownloadState( + link = intent.link, + downloadState = intent.downloadState, + ) + if (result > 0) { + updateDownloadInfoMap(intent.link) { copy(downloadState = intent.downloadState) } + } + }.catch { it.printStackTrace() }, + + filterIsInstance() + .debounceWithoutFirst(1000) + .onEach { intent -> + val result = downloadInfoDao.updateDownloadSize( + link = intent.link, size = intent.size, + ) + if (result > 0) { + updateDownloadInfoMap(intent.link) { copy(size = intent.size) } + } + }.catch { it.printStackTrace() }, + + filterIsInstance() + .debounceWithoutFirst(200) + .onEach { intent -> + if (intent.name.isNullOrBlank()) return@onEach + val result = downloadInfoDao.updateDownloadName( + link = intent.link, + name = intent.name, + ) + if (result > 0) { + updateDownloadInfoMap(intent.link) { copy(name = intent.name) } + } + }.catch { it.printStackTrace() }, + + filterIsInstance() + .onEach { intent -> + val result = downloadInfoDao.updateDownloadInfoRequestId( + link = intent.link, + downloadRequestId = intent.downloadRequestId, + ) + if (result > 0) { + updateDownloadInfoMap(intent.link) { + copy(downloadRequestId = intent.downloadRequestId) + } + } + }.catch { it.printStackTrace() }, + + filterIsInstance() + .onEach { intent -> torrentFileDao.updateTorrentFiles(intent.files) } + .catch { it.printStackTrace() }, + + filterIsInstance() + .debounceWithoutFirst(500) + .onEach { intent -> + val result = downloadInfoDao.updateDownloadDescription( + link = intent.link, + description = intent.description, + ) + if (result > 0) { + updateDownloadInfoMap(intent.link) { copy(description = intent.description) } + } + } + .catch { it.printStackTrace() }, + ) + } + + fun sendIntent(intent: DownloadManagerIntent) = scope.launch { + intentFlow.emit(intent) + } + suspend fun getDownloadInfoList(): Flow> { checkDownloadInfoMapInitialized() return downloadInfoListFlow @@ -169,97 +307,4 @@ class DownloadManager @Inject constructor( downloadInfoMap.remove(link) updateFlow() } - - suspend fun updateDownloadInfo(downloadInfoBean: DownloadInfoBean) { - downloadInfoDao.updateDownloadInfo(downloadInfoBean) - putDownloadInfoToMap(link = downloadInfoBean.link, newInfo = downloadInfoBean) - } - - suspend fun updateDownloadState( - link: String, - downloadState: DownloadState, - ): Int { - val result = downloadInfoDao.updateDownloadState( - link = link, - downloadState = downloadState, - ) - if (result > 0) { - updateDownloadInfoMap(link) { copy(downloadState = downloadState) } - } - return result - } - - suspend fun updateDownloadStateAndSessionParams( - link: String, - sessionStateData: ByteArray, - downloadState: DownloadState, - ) { - updateDownloadState(link, downloadState) - sessionParamsDao.updateSessionParams( - SessionParamsBean( - link = link, - data = sessionStateData, - ) - ) - } - - suspend fun updateDownloadDescription(link: String, description: String): Int { - val result = downloadInfoDao.updateDownloadDescription( - link = link, - description = description, - ) - if (result > 0) { - updateDownloadInfoMap(link) { copy(description = description) } - } - return result - } - - fun updateTorrentFiles(files: List) { - torrentFileDao.updateTorrentFiles(files) - } - - suspend fun updateDownloadName(link: String, name: String?): Int { - if (name.isNullOrBlank()) return 0 - val result = downloadInfoDao.updateDownloadName( - link = link, - name = name, - ) - if (result > 0) { - updateDownloadInfoMap(link) { copy(name = name) } - } - return result - } - - suspend fun updateDownloadProgress(link: String, progress: Float): Int { - val result = downloadInfoDao.updateDownloadProgress( - link = link, - progress = progress, - ) - if (result > 0) { - updateDownloadInfoMap(link) { copy(progress = progress) } - } - return result - } - - suspend fun updateDownloadSize(link: String, size: Long): Int { - val result = downloadInfoDao.updateDownloadSize( - link = link, - size = size, - ) - if (result > 0) { - updateDownloadInfoMap(link) { copy(size = size) } - } - return result - } - - suspend fun updateDownloadInfoRequestId(link: String, downloadRequestId: String): Int { - val result = downloadInfoDao.updateDownloadInfoRequestId( - link = link, - downloadRequestId = downloadRequestId, - ) - if (result > 0) { - updateDownloadInfoMap(link) { copy(downloadRequestId = downloadRequestId) } - } - return result - } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/repository/download/DownloadManagerIntent.kt b/app/src/main/java/com/skyd/anivu/model/repository/download/DownloadManagerIntent.kt new file mode 100644 index 00000000..f0eae705 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/repository/download/DownloadManagerIntent.kt @@ -0,0 +1,47 @@ +package com.skyd.anivu.model.repository.download + +import com.skyd.anivu.base.mvi.MviIntent +import com.skyd.anivu.model.bean.download.DownloadInfoBean +import com.skyd.anivu.model.bean.download.DownloadInfoBean.DownloadState +import com.skyd.anivu.model.bean.download.TorrentFileBean + +sealed interface DownloadManagerIntent : MviIntent { + data class UpdateDownloadInfo(val downloadInfoBean: DownloadInfoBean) : DownloadManagerIntent + data class UpdateSessionParams( + val link: String, + val sessionStateData: ByteArray, + ) : DownloadManagerIntent { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateSessionParams + + if (link != other.link) return false + if (!sessionStateData.contentEquals(other.sessionStateData)) return false + + return true + } + + override fun hashCode(): Int { + var result = link.hashCode() + result = 31 * result + sessionStateData.contentHashCode() + return result + } + } + + data class UpdateDownloadProgress(val link: String, val progress: Float) : DownloadManagerIntent + data class UpdateDownloadState( + val link: String, + val downloadState: DownloadState, + ) : DownloadManagerIntent + + data class UpdateDownloadSize(val link: String, val size: Long) : DownloadManagerIntent + data class UpdateDownloadName(val link: String, val name: String?) : DownloadManagerIntent + data class UpdateDownloadInfoRequestId(val link: String, val downloadRequestId: String) : + DownloadManagerIntent + + data class UpdateTorrentFiles(val files: List) : DownloadManagerIntent + data class UpdateDownloadDescription(val link: String, val description: String) : + DownloadManagerIntent +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/repository/download/DownloadRepository.kt b/app/src/main/java/com/skyd/anivu/model/repository/download/DownloadRepository.kt index 6a62fac5..8fd1c371 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/download/DownloadRepository.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/download/DownloadRepository.kt @@ -2,6 +2,7 @@ package com.skyd.anivu.model.repository.download import com.skyd.anivu.base.BaseRepository import com.skyd.anivu.config.Const +import com.skyd.anivu.ext.debounceWithoutFirst import com.skyd.anivu.model.bean.download.DownloadInfoBean import com.skyd.anivu.model.worker.download.DownloadTorrentWorker import kotlinx.coroutines.Dispatchers @@ -13,14 +14,12 @@ import kotlinx.coroutines.flow.flowOn import java.io.File import javax.inject.Inject -class DownloadRepository @Inject constructor( - private val downloadManager: DownloadManager, -) : BaseRepository() { +class DownloadRepository @Inject constructor() : BaseRepository() { suspend fun requestDownloadingVideos(): Flow> { return combine( - downloadManager.getDownloadInfoList().distinctUntilChanged(), - DownloadTorrentWorker.peerInfoMapFlow, - DownloadTorrentWorker.torrentStatusMapFlow, + DownloadManager.getDownloadInfoList().distinctUntilChanged(), + DownloadTorrentWorker.peerInfoMapFlow.debounceWithoutFirst(1000), + DownloadTorrentWorker.torrentStatusMapFlow.debounceWithoutFirst(1000), ) { list, peerInfoMap, uploadPayloadRateMap -> list.map { downloadInfoBean -> downloadInfoBean.copy().apply { @@ -39,19 +38,19 @@ class DownloadRepository @Inject constructor( link: String, ): Flow { return flow { - if (downloadManager.getDownloadState(link)?.downloadComplete() != true) { - downloadManager.getTorrentFilesByLink(link).forEach { + if (DownloadManager.getDownloadState(link)?.downloadComplete() != true) { + DownloadManager.getTorrentFilesByLink(link).forEach { File(it.path).deleteRecursively() } } - val requestUuid = downloadManager.getDownloadInfo(link)?.downloadRequestId + val requestUuid = DownloadManager.getDownloadInfo(link)?.downloadRequestId if (!requestUuid.isNullOrBlank()) { File(Const.TORRENT_RESUME_DATA_DIR, requestUuid).deleteRecursively() } // 这些最后删除,防止上面会使用 - downloadManager.deleteDownloadInfo(link) - downloadManager.deleteSessionParams(link) - downloadManager.removeDownloadLinkUuidMap(link) + DownloadManager.deleteDownloadInfo(link) + DownloadManager.deleteSessionParams(link) + DownloadManager.removeDownloadLinkUuidMap(link) emit(Unit) }.flowOn(Dispatchers.IO) } diff --git a/app/src/main/java/com/skyd/anivu/model/repository/feed/FeedRepository.kt b/app/src/main/java/com/skyd/anivu/model/repository/feed/FeedRepository.kt index 4489cf15..bb5e5323 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/feed/FeedRepository.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/feed/FeedRepository.kt @@ -2,16 +2,21 @@ package com.skyd.anivu.model.repository.feed import android.net.Uri import android.webkit.URLUtil +import com.skyd.anivu.appContext import com.skyd.anivu.base.BaseRepository import com.skyd.anivu.config.Const import com.skyd.anivu.ext.copyTo +import com.skyd.anivu.ext.dataStore import com.skyd.anivu.ext.isLocal import com.skyd.anivu.ext.isNetwork +import com.skyd.anivu.ext.put import com.skyd.anivu.model.bean.FeedBean import com.skyd.anivu.model.bean.GroupVo +import com.skyd.anivu.model.bean.GroupVo.Companion.isDefaultGroup import com.skyd.anivu.model.db.dao.ArticleDao import com.skyd.anivu.model.db.dao.FeedDao import com.skyd.anivu.model.db.dao.GroupDao +import com.skyd.anivu.model.preference.appearance.feed.FeedDefaultGroupExpandPreference import com.skyd.anivu.model.repository.RssHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -208,6 +213,20 @@ class FeedRepository @Inject constructor( }.flowOn(Dispatchers.IO) } + suspend fun changeGroupExpanded(group: GroupVo, expanded: Boolean): Flow { + return flow { + if (group.isDefaultGroup()) { + appContext.dataStore.put( + FeedDefaultGroupExpandPreference.key, + value = expanded, + ) + } else { + groupDao.changeGroupExpanded(group.groupId, expanded) + } + emit(Unit) + }.flowOn(Dispatchers.IO) + } + suspend fun readAllInGroup(groupId: String?): Flow { return flow { val realGroupId = if (groupId == GroupVo.DEFAULT_GROUP_ID) null else groupId diff --git a/app/src/main/java/com/skyd/anivu/model/worker/download/DownloadTorrentWorker.kt b/app/src/main/java/com/skyd/anivu/model/worker/download/DownloadTorrentWorker.kt index 749dea73..5ae05ce5 100644 --- a/app/src/main/java/com/skyd/anivu/model/worker/download/DownloadTorrentWorker.kt +++ b/app/src/main/java/com/skyd/anivu/model/worker/download/DownloadTorrentWorker.kt @@ -1,15 +1,25 @@ package com.skyd.anivu.model.worker.download +import android.Manifest import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE import android.os.Build import android.util.Log import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.work.CoroutineWorker import androidx.work.ExistingWorkPolicy @@ -20,6 +30,7 @@ import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkerParameters import androidx.work.workDataOf +import com.google.accompanist.permissions.rememberPermissionState import com.skyd.anivu.BuildConfig import com.skyd.anivu.R import com.skyd.anivu.appContext @@ -39,9 +50,12 @@ import com.skyd.anivu.model.bean.download.PeerInfoBean import com.skyd.anivu.model.preference.data.medialib.MediaLibLocationPreference import com.skyd.anivu.model.preference.transmission.SeedingWhenCompletePreference import com.skyd.anivu.model.repository.download.DownloadManager +import com.skyd.anivu.model.repository.download.DownloadManagerIntent import com.skyd.anivu.model.repository.download.DownloadRepository import com.skyd.anivu.model.service.HttpService +import com.skyd.anivu.model.worker.download.DownloadTorrentWorker.Companion.DownloadWorkStarter import com.skyd.anivu.ui.activity.MainActivity +import com.skyd.anivu.ui.component.showToast import com.skyd.anivu.ui.screen.download.DOWNLOAD_SCREEN_DEEP_LINK import com.skyd.anivu.util.uniqueInt import dagger.hilt.EntryPoint @@ -109,7 +123,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : private fun initData(): Boolean = runBlocking { torrentLinkUuid = inputData.getString(TORRENT_LINK_UUID) ?: return@runBlocking false - hiltEntryPoint.downloadManager.apply { + DownloadManager.apply { torrentLink = getDownloadLinkByUuid(torrentLinkUuid) ?: return@runBlocking false name = getDownloadName(link = torrentLink) progress = getDownloadProgress(link = torrentLink) ?: 0f @@ -141,7 +155,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : removeWorkerFromFlow(id.toString()) return Result.success( workDataOf( - STATE to (hiltEntryPoint.downloadManager.getDownloadState(link = torrentLink) + STATE to (DownloadManager.getDownloadState(link = torrentLink) ?.ordinal ?: 0), TORRENT_LINK_UUID to torrentLinkUuid, ) @@ -183,7 +197,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : private fun howToDownload(saveDir: File) = runBlocking { sessionManager.apply { - val lastSessionParams = hiltEntryPoint.downloadManager + val lastSessionParams = DownloadManager .getSessionParams(link = torrentLink) val sessionParams = if (lastSessionParams == null) SessionParams() else SessionParams(lastSessionParams.data) @@ -199,14 +213,16 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : start(sessionParams) startDht() - if (hiltEntryPoint.downloadManager.containsDownloadInfo(link = torrentLink)) { - hiltEntryPoint.downloadManager.updateDownloadInfoRequestId( - link = torrentLink, - downloadRequestId = id.toString(), + if (DownloadManager.containsDownloadInfo(link = torrentLink)) { + DownloadManager.sendIntent( + DownloadManagerIntent.UpdateDownloadInfoRequestId( + link = torrentLink, + downloadRequestId = id.toString(), + ) ) } var newDownloadState: DownloadInfoBean.DownloadState? = null - when (hiltEntryPoint.downloadManager.getDownloadState(link = torrentLink)) { + when (DownloadManager.getDownloadState(link = torrentLink)) { null, // 重新下载 DownloadInfoBean.DownloadState.Seeding, @@ -295,7 +311,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : applicationContext, MainActivity::class.java ), - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE, ) // Create a Notification channel if necessary @@ -398,7 +414,6 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : name = handle.name handle.saveResumeData() updateNotificationAsync() // update Notification - moveFromDownloadingDirToVideoDir(handle = handle) updateDownloadStateAndSessionParams( link = torrentLink, sessionStateData = sessionManager.saveState() ?: byteArrayOf(), @@ -490,7 +505,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : private fun pauseWorker( handle: TorrentHandle?, state: DownloadInfoBean.DownloadState = getWhatPausedState( - runBlocking { hiltEntryPoint.downloadManager.getDownloadState(link = torrentLink) } + runBlocking { DownloadManager.getDownloadState(link = torrentLink) } ) ) { if (!sessionManager.isRunning || sessionIsStopping) { @@ -514,14 +529,6 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : removeWorkerFromFlow(id.toString()) } - private fun moveFromDownloadingDirToVideoDir(handle: TorrentHandle) { -// if (handle.savePath() != Const.VIDEO_DIR.path) { -// handle.moveStorage(Const.VIDEO_DIR.path, MoveFlags.ALWAYS_REPLACE_FILES) -// } else { -// Log.w(TAG, "handle.savePath() != Const.VIDEO_DIR.path: ${handle.savePath()}") -// } - } - companion object { const val TAG = "DownloadTorrentWorker" const val STATE = "state" @@ -558,7 +565,6 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : @InstallIn(SingletonComponent::class) interface WorkerEntryPoint { val retrofit: Retrofit - val downloadManager: DownloadManager val downloadRepository: DownloadRepository } @@ -566,13 +572,59 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : appContext, WorkerEntryPoint::class.java ) + fun interface DownloadWorkStarter { + fun start(torrentLink: String, requestId: String?) + } + + @Composable + fun rememberDownloadWorkStarter(): DownloadWorkStarter { + val context = LocalContext.current + var currentTorrentLink: String? by rememberSaveable { mutableStateOf(null) } + var currentRequestId: String? by rememberSaveable { mutableStateOf(null) } + val starter = { startWorker(context, currentTorrentLink!!, currentRequestId) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val storagePermissionState = rememberPermissionState( + Manifest.permission.POST_NOTIFICATIONS + ) { + if (it) { + starter() + } else { + context.getString(R.string.download_no_notification_permission_tip) + .showToast() + } + } + return remember { + DownloadWorkStarter { torrentLink, requestId -> + currentTorrentLink = torrentLink + currentRequestId = requestId + storagePermissionState.launchPermissionRequest() + } + } + } else { + return remember { + DownloadWorkStarter { torrentLink, requestId -> + startWorker(context, torrentLink, requestId) + } + } + } + } + fun startWorker(context: Context, torrentLink: String, requestId: String? = null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission( + context, Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + context.getString(R.string.download_no_notification_permission_tip).showToast() + return + } + } coroutineScope.launch { var torrentLinkUuid = - hiltEntryPoint.downloadManager.getDownloadUuidByLink(torrentLink) + DownloadManager.getDownloadUuidByLink(torrentLink) if (torrentLinkUuid == null) { torrentLinkUuid = UUID.randomUUID().toString() - hiltEntryPoint.downloadManager.setDownloadLinkUuidMap( + DownloadManager.setDownloadLinkUuidMap( DownloadLinkUuidMapBean( link = torrentLink, uuid = torrentLinkUuid, @@ -623,7 +675,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : val workerState = getWorkInfoById(requestUuid).get()?.state if (workerState == null || workerState.isFinished) { coroutineScope.launch { - val state = hiltEntryPoint.downloadManager.getDownloadState(link) + val state = DownloadManager.getDownloadState(link) updateDownloadState( link = link, downloadState = getWhatPausedState(state), diff --git a/app/src/main/java/com/skyd/anivu/model/worker/download/Util.kt b/app/src/main/java/com/skyd/anivu/model/worker/download/Util.kt index 4d57a2c5..40d8e4cc 100644 --- a/app/src/main/java/com/skyd/anivu/model/worker/download/Util.kt +++ b/app/src/main/java/com/skyd/anivu/model/worker/download/Util.kt @@ -1,7 +1,6 @@ package com.skyd.anivu.model.worker.download import android.content.Context -import android.database.sqlite.SQLiteConstraintException import android.net.ConnectivityManager import android.net.ProxyInfo import android.util.Log @@ -21,6 +20,8 @@ import com.skyd.anivu.model.preference.proxy.ProxyPortPreference import com.skyd.anivu.model.preference.proxy.ProxyTypePreference import com.skyd.anivu.model.preference.proxy.ProxyUsernamePreference import com.skyd.anivu.model.preference.proxy.UseProxyPreference +import com.skyd.anivu.model.repository.download.DownloadManager +import com.skyd.anivu.model.repository.download.DownloadManagerIntent import kotlinx.coroutines.runBlocking import org.libtorrent4j.FileStorage import org.libtorrent4j.SettingsPack @@ -176,108 +177,87 @@ internal fun getWhatPausedState(oldState: DownloadInfoBean.DownloadState?) = internal fun updateDownloadState( link: String, downloadState: DownloadInfoBean.DownloadState, -): Boolean = runBlocking { - try { - val result = DownloadTorrentWorker.hiltEntryPoint.downloadManager.updateDownloadState( +) { + DownloadManager.sendIntent( + DownloadManagerIntent.UpdateDownloadState( link = link, downloadState = downloadState, ) - return@runBlocking result != 0 - } catch (e: SQLiteConstraintException) { - // 捕获link外键约束异常 - e.printStackTrace() - } - return@runBlocking false + ) } internal fun updateDownloadStateAndSessionParams( link: String, sessionStateData: ByteArray, downloadState: DownloadInfoBean.DownloadState, -) = runBlocking { - try { - DownloadTorrentWorker.hiltEntryPoint.downloadManager.updateDownloadStateAndSessionParams( - link = link, - sessionStateData = sessionStateData, - downloadState = downloadState, +) { + DownloadManager.sendIntent( + DownloadManagerIntent.UpdateDownloadState( + link = link, downloadState = downloadState, ) - } catch (e: SQLiteConstraintException) { - // 捕获link外键约束异常 - e.printStackTrace() - } + ) + DownloadManager.sendIntent( + DownloadManagerIntent.UpdateSessionParams( + link = link, sessionStateData = sessionStateData, + ) + ) } -internal fun updateDescriptionInfoToDb(link: String, description: String): Boolean = runBlocking { - val result = DownloadTorrentWorker.hiltEntryPoint.downloadManager.updateDownloadDescription( - link = link, - description = description, - ) - if (result == 0) { - Log.w( - DownloadTorrentWorker.TAG, - "updateDownloadDescription return 0. description: $description" +internal fun updateDescriptionInfoToDb(link: String, description: String) { + DownloadManager.sendIntent( + DownloadManagerIntent.UpdateDownloadDescription( + link = link, + description = description, ) - } - return@runBlocking result != 0 + ) } internal fun updateTorrentFilesToDb( link: String, savePath: String, files: FileStorage, -): Boolean { - DownloadTorrentWorker.hiltEntryPoint.downloadManager.apply { - val list = mutableListOf() - runCatching { - for (i in 0..() + runCatching { + for (i in 0.. Int, - data: (index: Int) -> Any, - listState: LazyGridState = rememberLazyGridState(), - adapter: LazyGridAdapter, - reverseLayout: Boolean = false, - verticalArrangement: Arrangement.Vertical = - if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, - horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, - key: ((index: Int, item: Any) -> Any)? = null -) { - LazyVerticalGrid( - modifier = modifier, - columns = columns, - state = listState, - contentPadding = contentPadding, - reverseLayout = reverseLayout, - verticalArrangement = verticalArrangement, - horizontalArrangement = horizontalArrangement, - ) { - items( - count = count(), - key = if (key == null) null else { index -> key.invoke(index, data(index)) }, - ) { index -> - adapter.Draw( - index = index, - data = data(index) - ) - } - } -} - -@Composable -fun AniVuLazyVerticalGrid( - modifier: Modifier = Modifier, - columns: GridCells, - contentPadding: PaddingValues = PaddingValues(), - dataList: LazyPagingItems<*>, - listState: LazyGridState = rememberLazyGridState(), - adapter: LazyGridAdapter, - reverseLayout: Boolean = false, - verticalArrangement: Arrangement.Vertical = - if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, - horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, - key: ((index: Int, item: Any) -> Any)? = null -) { - AniVuLazyVerticalGrid( - modifier = modifier, - columns = columns, - listState = listState, - contentPadding = contentPadding, - count = { dataList.itemCount }, - data = { dataList[it]!! }, - adapter = adapter, - reverseLayout = reverseLayout, - verticalArrangement = verticalArrangement, - horizontalArrangement = horizontalArrangement, - key = key, - ) -} - -@Composable -fun AniVuLazyVerticalGrid( - modifier: Modifier = Modifier, - columns: GridCells, - contentPadding: PaddingValues = PaddingValues(), - dataList: Collection, - listState: LazyGridState = rememberLazyGridState(), - adapter: LazyGridAdapter, - reverseLayout: Boolean = false, - verticalArrangement: Arrangement.Vertical = - if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, - horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, - key: ((index: Int, item: Any) -> Any)? = null -) { - AniVuLazyVerticalGrid( - modifier = modifier, - columns = columns, - listState = listState, - contentPadding = contentPadding, - count = { dataList.size }, - data = { dataList.elementAt(it) }, - adapter = adapter, - reverseLayout = reverseLayout, - verticalArrangement = verticalArrangement, - horizontalArrangement = horizontalArrangement, - key = key, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/LazyGridAdapter.kt b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/LazyGridAdapter.kt deleted file mode 100644 index 9b923826..00000000 --- a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/LazyGridAdapter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.skyd.anivu.ui.component.lazyverticalgrid.adapter - -import androidx.compose.runtime.Composable -import java.lang.reflect.ParameterizedType - -class LazyGridAdapter( - private var proxyList: MutableList> = mutableListOf(), -) { - @Suppress("UNCHECKED_CAST") - @Composable - fun Draw(index: Int, data: Any) { - val type: Int = getProxyIndex(data) - if (type != -1) (proxyList[type] as Proxy).Draw(index, data) - } - - // 获取策略在列表中的索引,可能返回-1 - private fun getProxyIndex(data: Any): Int = proxyList.indexOfFirst { - // 如果Proxy中的第一个类型参数T和数据的类型相同,则返回对应策略的索引 - (it.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0].let { argument -> - if (argument.toString() == data.javaClass.toString()) - true // 正常情况 - else if (((argument as? ParameterizedType)?.rawType as? Class<*>) - ?.isAssignableFrom(data.javaClass) == true - ) { - true // data是T的子类的情况 - } else { - // Proxy第一个泛型是类似List,又嵌套了个泛型 - if (argument is ParameterizedType) - argument.rawType.toString() == data.javaClass.toString() - else false - } - } - } - - // 抽象策略类 - abstract class Proxy { - @Composable - abstract fun Draw(index: Int, data: T) - } -} diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/DefaultGroup1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/DefaultGroup1Proxy.kt deleted file mode 100644 index 1337db9b..00000000 --- a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/DefaultGroup1Proxy.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy - -import androidx.compose.runtime.Composable -import com.skyd.anivu.model.bean.GroupVo -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter - -class DefaultGroup1Proxy( - private val group1Proxy: Group1Proxy, - private val hide: (index: Int) -> Boolean, -) : LazyGridAdapter.Proxy() { - @Composable - override fun Draw(index: Int, data: GroupVo.DefaultGroup) { - if (!hide(index)) { - Group1Item( - index = index, - data = data, - initExpand = group1Proxy.isExpand, - onExpandChange = group1Proxy.onExpandChange, - isEmpty = group1Proxy.isEmpty, - onShowAllArticles = group1Proxy.onShowAllArticles, - onEdit = group1Proxy.onEdit, - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt b/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt index 3f2f86ef..c913d111 100644 --- a/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt +++ b/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt @@ -15,7 +15,7 @@ import com.skyd.anivu.model.preference.appearance.article.ArticleListTonalElevat import com.skyd.anivu.model.preference.appearance.article.ArticleTopBarTonalElevationPreference import com.skyd.anivu.model.preference.appearance.article.ShowArticlePullRefreshPreference import com.skyd.anivu.model.preference.appearance.article.ShowArticleTopBarRefreshPreference -import com.skyd.anivu.model.preference.appearance.feed.FeedGroupExpandPreference +import com.skyd.anivu.model.preference.appearance.feed.FeedDefaultGroupExpandPreference import com.skyd.anivu.model.preference.appearance.feed.FeedListTonalElevationPreference import com.skyd.anivu.model.preference.appearance.feed.FeedTopBarTonalElevationPreference import com.skyd.anivu.model.preference.appearance.media.MediaShowThumbnailPreference @@ -68,7 +68,7 @@ val LocalWindowSizeClass = compositionLocalOf { // Appearance val LocalTheme = compositionLocalOf { ThemePreference.default } val LocalDarkMode = compositionLocalOf { DarkModePreference.default } -val LocalFeedGroupExpand = compositionLocalOf { FeedGroupExpandPreference.default } +val LocalFeedDefaultGroupExpand = compositionLocalOf { FeedDefaultGroupExpandPreference.default } val LocalTextFieldStyle = compositionLocalOf { TextFieldStylePreference.default } val LocalDateStyle = compositionLocalOf { DateStylePreference.default } val LocalNavigationBarLabel = compositionLocalOf { NavigationBarLabelPreference.default } diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/screen/article/Article1Item.kt similarity index 98% rename from app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt rename to app/src/main/java/com/skyd/anivu/ui/screen/article/Article1Item.kt index f7722b3f..ff6f1d6f 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/article/Article1Item.kt @@ -1,4 +1,4 @@ -package com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy +package com.skyd.anivu.ui.screen.article import androidx.compose.animation.Animatable import androidx.compose.foundation.background @@ -68,36 +68,26 @@ import com.skyd.anivu.ext.dataStore import com.skyd.anivu.ext.getOrDefault import com.skyd.anivu.ext.readable import com.skyd.anivu.ext.toDateTimeString +import com.skyd.anivu.model.bean.FeedBean import com.skyd.anivu.model.bean.article.ArticleBean import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean import com.skyd.anivu.model.bean.article.ArticleWithFeed -import com.skyd.anivu.model.bean.FeedBean import com.skyd.anivu.model.preference.behavior.article.ArticleSwipeActionPreference import com.skyd.anivu.model.preference.behavior.article.ArticleSwipeLeftActionPreference import com.skyd.anivu.model.preference.behavior.article.ArticleSwipeRightActionPreference import com.skyd.anivu.model.preference.behavior.article.ArticleTapActionPreference import com.skyd.anivu.ui.component.AniVuImage -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter import com.skyd.anivu.ui.component.rememberAniVuImageLoader -import com.skyd.anivu.ui.screen.article.enclosure.EnclosureBottomSheet -import com.skyd.anivu.ui.screen.article.enclosure.getEnclosuresList -import com.skyd.anivu.ui.screen.read.openReadScreen import com.skyd.anivu.ui.local.LocalArticleItemTonalElevation import com.skyd.anivu.ui.local.LocalArticleSwipeLeftAction import com.skyd.anivu.ui.local.LocalArticleSwipeRightAction import com.skyd.anivu.ui.local.LocalArticleTapAction import com.skyd.anivu.ui.local.LocalDeduplicateTitleInDesc import com.skyd.anivu.ui.local.LocalNavController +import com.skyd.anivu.ui.screen.article.enclosure.EnclosureBottomSheet +import com.skyd.anivu.ui.screen.article.enclosure.getEnclosuresList +import com.skyd.anivu.ui.screen.read.openReadScreen -class Article1Proxy( - private val onFavorite: (ArticleWithFeed, Boolean) -> Unit, - private val onRead: (ArticleWithFeed, Boolean) -> Unit, -) : LazyGridAdapter.Proxy() { - @Composable - override fun Draw(index: Int, data: ArticleWithFeed) { - Article1Item(data = data, onFavorite = onFavorite, onRead = onRead) - } -} @Composable fun Article1Item( diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticleScreen.kt b/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticleScreen.kt index 9c6384cc..90f96809 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticleScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticleScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -78,9 +79,6 @@ import com.skyd.anivu.ui.component.CircularProgressPlaceholder import com.skyd.anivu.ui.component.EmptyPlaceholder import com.skyd.anivu.ui.component.dialog.AniVuDialog import com.skyd.anivu.ui.component.dialog.WaitingDialog -import com.skyd.anivu.ui.component.lazyverticalgrid.AniVuLazyVerticalGrid -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy.Article1Proxy import com.skyd.anivu.ui.local.LocalArticleItemMinWidth import com.skyd.anivu.ui.local.LocalArticleListTonalElevation import com.skyd.anivu.ui.local.LocalArticleTopBarTonalElevation @@ -366,20 +364,27 @@ private fun ArticleList( contentPadding: PaddingValues, ) { if (articles.itemCount > 0) { - val adapter = remember { - LazyGridAdapter(mutableListOf(Article1Proxy(onFavorite = onFavorite, onRead = onRead))) - } - AniVuLazyVerticalGrid( + LazyVerticalGrid( modifier = modifier.fillMaxSize(), columns = GridCells.Adaptive(LocalArticleItemMinWidth.current.dp), - dataList = articles, - listState = listState, - adapter = adapter, + state = listState, contentPadding = contentPadding + PaddingValues(horizontal = 12.dp, vertical = 6.dp), verticalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), - key = { _, item -> (item as ArticleWithFeed).articleWithEnclosure.article.articleId }, - ) + ) { + items( + count = articles.itemCount, + key = { index -> (articles[index] as ArticleWithFeed).articleWithEnclosure.article.articleId }, + ) { index -> + when (val item = articles[index]) { + is ArticleWithFeed -> Article1Item( + data = item, + onFavorite = onFavorite, + onRead = onRead, + ) + } + } + } } else { EmptyPlaceholder( modifier = Modifier.verticalScroll(rememberScrollState()), diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/article/enclosure/EnclosureBottomSheet.kt b/app/src/main/java/com/skyd/anivu/ui/screen/article/enclosure/EnclosureBottomSheet.kt index f9dd8ec3..9e9ffe60 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/article/enclosure/EnclosureBottomSheet.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/article/enclosure/EnclosureBottomSheet.kt @@ -39,7 +39,7 @@ import com.skyd.anivu.model.bean.LinkEnclosureBean import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean import com.skyd.anivu.model.bean.article.EnclosureBean import com.skyd.anivu.model.preference.rss.ParseLinkTagAsEnclosurePreference -import com.skyd.anivu.model.worker.download.DownloadTorrentWorker +import com.skyd.anivu.model.worker.download.DownloadTorrentWorker.Companion.rememberDownloadWorkStarter import com.skyd.anivu.model.worker.download.doIfMagnetOrTorrentLink import com.skyd.anivu.model.worker.download.isTorrentMimetype import com.skyd.anivu.ui.activity.PlayActivity @@ -68,7 +68,7 @@ fun EnclosureBottomSheet( sheetState: SheetState = rememberModalBottomSheetState(), dataList: List, ) { - val context = LocalContext.current + val downloadWorkStarter = rememberDownloadWorkStarter() val onDownload: (Any) -> Unit = remember { { val url = when (it) { @@ -77,9 +77,7 @@ fun EnclosureBottomSheet( else -> null } if (!url.isNullOrBlank()) { - DownloadTorrentWorker.startWorker( - context = context, torrentLink = url - ) + downloadWorkStarter.start(torrentLink = url, requestId = null) } } } diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadIntent.kt b/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadIntent.kt index 096f7b83..88abc149 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadIntent.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadIntent.kt @@ -4,5 +4,4 @@ import com.skyd.anivu.base.mvi.MviIntent sealed interface DownloadIntent : MviIntent { data object Init : DownloadIntent - data class AddDownload(val link: String) : DownloadIntent } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadPartialStateChange.kt b/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadPartialStateChange.kt index a991e378..c356a75a 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadPartialStateChange.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadPartialStateChange.kt @@ -37,21 +37,4 @@ internal sealed interface DownloadPartialStateChange { data class Failed(val msg: String) : DownloadListResult data object Loading : DownloadListResult } - - sealed interface AddDownloadResult : DownloadPartialStateChange { - override fun reduce(oldState: DownloadState): DownloadState { - return when (this) { - is Success -> oldState.copy( - loadingDialog = false, - ) - - is Failed -> oldState.copy( - loadingDialog = false, - ) - } - } - - data object Success : AddDownloadResult - data class Failed(val msg: String) : AddDownloadResult - } } diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadScreen.kt b/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadScreen.kt index c7811a9e..d068f3ba 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadScreen.kt @@ -3,9 +3,7 @@ package com.skyd.anivu.ui.screen.download import android.os.Bundle import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Download @@ -40,6 +38,7 @@ import com.skyd.anivu.ext.plus import com.skyd.anivu.ext.showSnackbar import com.skyd.anivu.model.bean.download.DownloadInfoBean import com.skyd.anivu.model.worker.download.DownloadTorrentWorker +import com.skyd.anivu.model.worker.download.DownloadTorrentWorker.Companion.rememberDownloadWorkStarter import com.skyd.anivu.model.worker.download.doIfMagnetOrTorrentLink import com.skyd.anivu.ui.component.AniVuFloatingActionButton import com.skyd.anivu.ui.component.AniVuTopBar @@ -81,7 +80,7 @@ fun DownloadScreen(downloadLink: String? = null, viewModel: DownloadViewModel = var fabHeight by remember { mutableStateOf(0.dp) } val uiState by viewModel.viewState.collectAsStateWithLifecycle() - val dispatch = viewModel.getDispatcher(startWith = DownloadIntent.Init) + viewModel.getDispatcher(startWith = DownloadIntent.Init) Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, @@ -115,6 +114,7 @@ fun DownloadScreen(downloadLink: String? = null, viewModel: DownloadViewModel = } } + val downloadWorkStarter = rememberDownloadWorkStarter() TextFieldDialog( visible = openLinkDialog != null, icon = { Icon(imageVector = Icons.Outlined.Download, contentDescription = null) }, @@ -127,8 +127,8 @@ fun DownloadScreen(downloadLink: String? = null, viewModel: DownloadViewModel = openLinkDialog = null doIfMagnetOrTorrentLink( link = text, - onMagnet = { dispatch(DownloadIntent.AddDownload(it)) }, - onTorrent = { dispatch(DownloadIntent.AddDownload(it)) }, + onMagnet = { downloadWorkStarter.start(torrentLink = it, requestId = null) }, + onTorrent = { downloadWorkStarter.start(torrentLink = it, requestId = null) }, onUnsupported = { snackbarHostState.showSnackbar( scope = scope, @@ -148,6 +148,7 @@ private fun DownloadList( ) { if (downloadInfoBeanList.isNotEmpty()) { val context = LocalContext.current + val downloadWorkStarter = rememberDownloadWorkStarter() LazyColumn( modifier = Modifier.nestedScroll(nestedScrollConnection), contentPadding = contentPadding, @@ -167,8 +168,7 @@ private fun DownloadList( ) }, onResume = { video -> - DownloadTorrentWorker.startWorker( - context = context, + downloadWorkStarter.start( torrentLink = video.link, requestId = video.downloadRequestId, ) diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadViewModel.kt b/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadViewModel.kt index b8e573d7..cb165a90 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadViewModel.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/download/DownloadViewModel.kt @@ -58,17 +58,6 @@ class DownloadViewModel @Inject constructor( }.startWith(DownloadPartialStateChange.DownloadListResult.Loading) .catchMap { DownloadPartialStateChange.DownloadListResult.Failed(it.message.toString()) } }, - filterIsInstance().flatMapConcat { intent -> - flow { - DownloadTorrentWorker.startWorker( - context = appContext, - torrentLink = intent.link, - ) - }.map { - DownloadPartialStateChange.AddDownloadResult.Success - }.startWith(DownloadPartialStateChange.LoadingDialog.Show) - .catchMap { DownloadPartialStateChange.AddDownloadResult.Failed(it.message.toString()) } - }, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/feed/EditFeedSheet.kt b/app/src/main/java/com/skyd/anivu/ui/screen/feed/EditFeedSheet.kt index 46c580b4..7813ecd8 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/feed/EditFeedSheet.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/feed/EditFeedSheet.kt @@ -73,7 +73,7 @@ import com.skyd.anivu.ui.component.AniVuIconButton import com.skyd.anivu.ui.component.dialog.AniVuDialog import com.skyd.anivu.ui.component.dialog.DeleteWarningDialog import com.skyd.anivu.ui.component.dialog.TextFieldDialog -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy.FeedIcon +import com.skyd.anivu.ui.screen.article.FeedIcon import com.skyd.anivu.ui.component.showToast import com.skyd.anivu.ui.local.LocalNavController import com.skyd.anivu.ui.screen.feed.requestheaders.openRequestHeadersScreen diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedIntent.kt b/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedIntent.kt index 3fdef6d0..f6ecdbf1 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedIntent.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedIntent.kt @@ -28,6 +28,7 @@ sealed interface FeedIntent : MviIntent { data class RefreshFeed(val url: String) : FeedIntent data class RefreshGroupFeed(val groupId: String?) : FeedIntent data class CreateGroup(val group: GroupVo) : FeedIntent + data class ChangeGroupExpanded(val group: GroupVo, val expanded: Boolean) : FeedIntent data class ClearGroupArticles(val groupId: String) : FeedIntent data class DeleteGroup(val groupId: String) : FeedIntent data class RenameGroup(val groupId: String, val name: String) : FeedIntent diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedPartialStateChange.kt b/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedPartialStateChange.kt index a9f859ea..d52feeb3 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedPartialStateChange.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedPartialStateChange.kt @@ -132,6 +132,13 @@ internal sealed interface FeedPartialStateChange { data class Failed(val msg: String) : CreateGroup } + sealed interface GroupExpandedChanged : FeedPartialStateChange { + override fun reduce(oldState: FeedState): FeedState = oldState + + data object Success : GroupExpandedChanged + data class Failed(val msg: String) : GroupExpandedChanged + } + sealed interface ClearGroupArticles : FeedPartialStateChange { override fun reduce(oldState: FeedState): FeedState { return when (this) { diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedScreen.kt b/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedScreen.kt index 6848be17..be50dfaf 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedScreen.kt @@ -1,6 +1,7 @@ package com.skyd.anivu.ui.screen.feed import androidx.activity.compose.BackHandler +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets @@ -11,6 +12,8 @@ import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Article import androidx.compose.material.icons.automirrored.outlined.Sort @@ -41,9 +44,7 @@ import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -66,17 +67,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.skyd.anivu.R import com.skyd.anivu.base.mvi.MviEventListener import com.skyd.anivu.base.mvi.getDispatcher -import com.skyd.anivu.ext.dataStore -import com.skyd.anivu.ext.getOrDefault import com.skyd.anivu.ext.isCompact import com.skyd.anivu.ext.plus -import com.skyd.anivu.ext.snapshotStateMapSaver import com.skyd.anivu.model.bean.FeedBean import com.skyd.anivu.model.bean.FeedBean.Companion.isDefaultGroup import com.skyd.anivu.model.bean.FeedViewBean import com.skyd.anivu.model.bean.GroupVo import com.skyd.anivu.model.bean.GroupVo.Companion.isDefaultGroup -import com.skyd.anivu.model.preference.appearance.feed.FeedGroupExpandPreference import com.skyd.anivu.ui.component.AniVuFloatingActionButton import com.skyd.anivu.ui.component.AniVuIconButton import com.skyd.anivu.ui.component.AniVuTopBar @@ -85,13 +82,8 @@ import com.skyd.anivu.ui.component.ClipboardTextField import com.skyd.anivu.ui.component.dialog.AniVuDialog import com.skyd.anivu.ui.component.dialog.TextFieldDialog import com.skyd.anivu.ui.component.dialog.WaitingDialog -import com.skyd.anivu.ui.component.lazyverticalgrid.AniVuLazyVerticalGrid -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy.DefaultGroup1Proxy -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy.Feed1Proxy -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy.Group1Proxy import com.skyd.anivu.ui.component.showToast -import com.skyd.anivu.ui.local.LocalFeedGroupExpand +import com.skyd.anivu.ui.local.LocalFeedDefaultGroupExpand import com.skyd.anivu.ui.local.LocalFeedListTonalElevation import com.skyd.anivu.ui.local.LocalFeedTopBarTonalElevation import com.skyd.anivu.ui.local.LocalHideEmptyDefault @@ -99,6 +91,8 @@ import com.skyd.anivu.ui.local.LocalNavController import com.skyd.anivu.ui.local.LocalWindowSizeClass import com.skyd.anivu.ui.screen.article.ArticleScreen import com.skyd.anivu.ui.screen.article.openArticleScreen +import com.skyd.anivu.ui.screen.feed.item.Feed1Item +import com.skyd.anivu.ui.screen.feed.item.Group1Item import com.skyd.anivu.ui.screen.feed.reorder.REORDER_GROUP_SCREEN_ROUTE import com.skyd.anivu.ui.screen.search.SearchDomain import com.skyd.anivu.ui.screen.search.openSearchScreen @@ -275,6 +269,9 @@ private fun FeedList( contentPadding = innerPadding + PaddingValues(bottom = fabHeight + 16.dp), selectedFeedUrls = listPaneSelectedFeedUrls, onShowArticleList = { feedUrls -> onShowArticleList(feedUrls) }, + onExpandChanged = { group, expanded -> + dispatch(FeedIntent.ChangeGroupExpanded(group, expanded)) + }, onEditFeed = { feed -> openEditFeedDialog = feed }, onEditGroup = { group -> openEditGroupDialog = group }, ) @@ -549,83 +546,67 @@ private fun FeedList( contentPadding: PaddingValues = PaddingValues(), selectedFeedUrls: List? = null, onShowArticleList: (List) -> Unit, + onExpandChanged: (GroupVo, Boolean) -> Unit, onEditFeed: (FeedBean) -> Unit, onEditGroup: (GroupVo) -> Unit, ) { - val context = LocalContext.current val hideEmptyDefault = LocalHideEmptyDefault.current - val feedGroupExpand = LocalFeedGroupExpand.current - val groups by remember { derivedStateOf { result.filterIsInstance() } } - val feedVisible = rememberSaveable(saver = snapshotStateMapSaver()) { - mutableStateMapOf( - GroupVo.DEFAULT_GROUP_ID to feedGroupExpand, - *(groups - .map { it.groupId to feedGroupExpand } - .toTypedArray()) - ) - } - // Update feedVisible when groups change - LaunchedEffect(groups) { - feedVisible.forEach { (t, u) -> - if (groups.find { it.groupId == t } == null) { - feedVisible.remove(t) - } else { - feedVisible[t] = u - } - } - val defaultExpand = context.dataStore.getOrDefault(FeedGroupExpandPreference) - groups.forEach { - feedVisible[it.groupId] = feedVisible[it.groupId] ?: defaultExpand - } - } + val groups = remember(result) { result.filterIsInstance() } + val idToGroup = remember(groups) { groups.associateBy { it.groupId } } - val adapter = remember(result, hideEmptyDefault, selectedFeedUrls) { - val group1Proxy = Group1Proxy( - isExpand = { feedVisible[it.groupId] ?: false }, - onExpandChange = { data, expand -> feedVisible[data.groupId] = expand }, - isEmpty = { it == result.lastIndex || result[it + 1] is GroupVo }, - onShowAllArticles = { group -> - val feedUrls = result - .filterIsInstance() - .filter { it.feed.groupId == group.groupId || group.isDefaultGroup() && it.feed.isDefaultGroup() } - .map { it.feed.url } - onShowArticleList(feedUrls) - }, - onEdit = onEditGroup, - ) - LazyGridAdapter( - mutableListOf( - DefaultGroup1Proxy( - group1Proxy = group1Proxy, - hide = { hideEmptyDefault && result.getOrNull(it + 1) !is FeedViewBean }, - ), - group1Proxy, - Feed1Proxy( - visible = { feedVisible[it] ?: false }, - selected = { selectedFeedUrls != null && it.url in selectedFeedUrls }, - inGroup = { true }, - onClick = { onShowArticleList(listOf(it.url)) }, - isEnded = { it == result.lastIndex || result[it + 1] is GroupVo }, - onEdit = onEditFeed - ) - ) - ) + val defaultGroupShouldHide: (Int) -> Boolean = { index -> + hideEmptyDefault && result.getOrNull(index + 1) !is FeedViewBean } - AniVuLazyVerticalGrid( + LazyVerticalGrid( modifier = modifier.fillMaxSize(), columns = GridCells.Fixed(1), - dataList = result, - adapter = adapter, contentPadding = contentPadding + PaddingValues(horizontal = 16.dp), - key = { _, item -> + ) { + itemsIndexed( + items = result, + key = { _, item -> + when (item) { + is GroupVo.DefaultGroup -> item.groupId + is GroupVo -> item.groupId + is FeedViewBean -> item.feed.url + else -> item + } + }, + ) { index, item -> when (item) { - is GroupVo.DefaultGroup -> item.groupId - is GroupVo -> item.groupId - is FeedViewBean -> item.feed.url - else -> item + is GroupVo -> if (!(item.isDefaultGroup() && defaultGroupShouldHide(index))) { + Group1Item( + index = index, + data = item, + onExpandChange = onExpandChanged, + isEmpty = { it == result.lastIndex || result[it + 1] is GroupVo }, + onShowAllArticles = { group -> + val feedUrls = result + .filterIsInstance() + .filter { it.feed.groupId == group.groupId || group.isDefaultGroup() && it.feed.isDefaultGroup() } + .map { it.feed.url } + onShowArticleList(feedUrls) + }, + onEdit = onEditGroup, + ) + } + + is FeedViewBean -> Feed1Item( + data = item, + visible = if (item.feed.groupId == null) { + LocalFeedDefaultGroupExpand.current + } else { + idToGroup[item.feed.groupId]?.isExpanded ?: false + }, + selected = selectedFeedUrls != null && item.feed.url in selectedFeedUrls, + inGroup = true, + isEnd = index == result.lastIndex || result[index + 1] is GroupVo, + onClick = { onShowArticleList(listOf(it.url)) }, + onEdit = onEditFeed, + ) } - }, - ) + } + } } @Composable diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedViewModel.kt b/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedViewModel.kt index 55df9ad2..71b92cc6 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedViewModel.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedViewModel.kt @@ -209,6 +209,13 @@ class FeedViewModel @Inject constructor( }.startWith(FeedPartialStateChange.LoadingDialog.Show) .catchMap { FeedPartialStateChange.CreateGroup.Failed(it.message.toString()) } }, + filterIsInstance().flatMapConcat { intent -> + feedRepo.changeGroupExpanded(intent.group, intent.expanded).map { + FeedPartialStateChange.GroupExpandedChanged.Success + }.catchMap { + FeedPartialStateChange.GroupExpandedChanged.Failed(it.message.toString()) + } + }, filterIsInstance().flatMapConcat { intent -> feedRepo.clearGroupArticles(intent.groupId).map { FeedPartialStateChange.ClearGroupArticles.Success diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Feed1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/screen/feed/item/Feed1Item.kt similarity index 78% rename from app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Feed1Proxy.kt rename to app/src/main/java/com/skyd/anivu/ui/screen/feed/item/Feed1Item.kt index 7d385d7b..0fff4061 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Feed1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/feed/item/Feed1Item.kt @@ -1,4 +1,4 @@ -package com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy +package com.skyd.anivu.ui.screen.feed.item import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically @@ -27,58 +27,32 @@ import androidx.compose.ui.unit.dp import com.skyd.anivu.ext.readable import com.skyd.anivu.model.bean.FeedBean import com.skyd.anivu.model.bean.FeedViewBean -import com.skyd.anivu.model.bean.GroupVo -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter import com.skyd.anivu.ui.local.LocalNavController +import com.skyd.anivu.ui.screen.article.FeedIcon import com.skyd.anivu.ui.screen.article.openArticleScreen -class Feed1Proxy( - private val visible: (groupId: String) -> Boolean = { true }, - private val selected: (FeedBean) -> Boolean = { false }, - private val isEnded: (index: Int) -> Boolean = { false }, - private val inGroup: () -> Boolean = { false }, - private val onClick: ((FeedBean) -> Unit)? = null, - private val onEdit: ((FeedBean) -> Unit)? = null, -) : LazyGridAdapter.Proxy() { - @Composable - override fun Draw(index: Int, data: FeedViewBean) { - Feed1Item( - index = index, - data = data, - visible = visible, - selected = selected, - isEnded = isEnded, - inGroup = inGroup, - onClick = onClick, - onEdit = onEdit, - ) - } -} - @Composable fun Feed1Item( - index: Int, data: FeedViewBean, - visible: (groupId: String) -> Boolean, - selected: (FeedBean) -> Boolean, - inGroup: () -> Boolean, + visible: Boolean = true, + selected: Boolean = false, + inGroup: Boolean = false, + isEnd: Boolean = false, onClick: ((FeedBean) -> Unit)? = null, - isEnded: (index: Int) -> Boolean, onEdit: ((FeedBean) -> Unit)? = null, ) { val navController = LocalNavController.current val feed = data.feed AnimatedVisibility( - visible = visible(feed.groupId ?: GroupVo.DEFAULT_GROUP_ID), + visible = visible, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically(), ) { - val isEnd = isEnded(index) Row( modifier = Modifier .clip( - if (inGroup()) { + if (inGroup) { if (isEnd) RoundedCornerShape(0.dp, 0.dp, SHAPE_CORNER_DP, SHAPE_CORNER_DP) else RectangleShape } else { @@ -87,7 +61,7 @@ fun Feed1Item( ) .background( MaterialTheme.colorScheme.secondary.copy( - alpha = if (selected(feed)) 0.15f else 0.1f + alpha = if (selected) 0.15f else 0.1f ) ) .combinedClickable( @@ -104,7 +78,7 @@ fun Feed1Item( }, ) .padding(horizontal = 20.dp, vertical = 10.dp) - .padding(bottom = if (inGroup() && isEnd) 6.dp else 0.dp) + .padding(bottom = if (inGroup && isEnd) 6.dp else 0.dp) ) { FeedIcon(modifier = Modifier.padding(vertical = 3.dp), data = feed, size = 36.dp) Spacer(modifier = Modifier.width(12.dp)) diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Group1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/screen/feed/item/Group1Item.kt similarity index 55% rename from app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Group1Proxy.kt rename to app/src/main/java/com/skyd/anivu/ui/screen/feed/item/Group1Item.kt index 2f675fea..dbacdf77 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Group1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/feed/item/Group1Item.kt @@ -1,8 +1,9 @@ -package com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy +package com.skyd.anivu.ui.screen.feed.item import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -13,38 +14,16 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.skyd.anivu.model.bean.GroupVo +import com.skyd.anivu.model.bean.GroupVo.Companion.isDefaultGroup import com.skyd.anivu.ui.component.AniVuIconButton -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter +import com.skyd.anivu.ui.local.LocalFeedDefaultGroupExpand -open class Group1Proxy( - val isExpand: (GroupVo) -> Boolean = { false }, - val onExpandChange: (GroupVo, Boolean) -> Unit = { _, _ -> }, - val isEmpty: (index: Int) -> Boolean, - val onShowAllArticles: (GroupVo) -> Unit = { }, - val onEdit: ((GroupVo) -> Unit)? = null, -) : LazyGridAdapter.Proxy() { - @Composable - override fun Draw(index: Int, data: GroupVo) { - Group1Item( - index = index, - data = data, - initExpand = isExpand, - onExpandChange = onExpandChange, - isEmpty = isEmpty, - onShowAllArticles = onShowAllArticles, - onEdit = onEdit, - ) - } -} val SHAPE_CORNER_DP = 26.dp @@ -52,39 +31,39 @@ val SHAPE_CORNER_DP = 26.dp fun Group1Item( index: Int, data: GroupVo, - initExpand: (GroupVo) -> Boolean = { false }, onExpandChange: (GroupVo, Boolean) -> Unit, isEmpty: (index: Int) -> Boolean, onShowAllArticles: (GroupVo) -> Unit, onEdit: ((GroupVo) -> Unit)? = null, ) { - var expand by remember(data) { mutableStateOf(initExpand(data)) } - + val isExpanded = + if (data.isDefaultGroup()) LocalFeedDefaultGroupExpand.current else data.isExpanded val backgroundShapeCorner: Dp by animateDpAsState( - targetValue = if (expand && !isEmpty(index)) 0.dp else SHAPE_CORNER_DP, + targetValue = if (isExpanded && !isEmpty(index)) 0.dp else SHAPE_CORNER_DP, label = "background shape corner", ) + val shape = RoundedCornerShape( + SHAPE_CORNER_DP, + SHAPE_CORNER_DP, + backgroundShapeCorner, + backgroundShapeCorner, + ) Row( modifier = Modifier .padding(top = 16.dp) - .clip( - RoundedCornerShape( - SHAPE_CORNER_DP, - SHAPE_CORNER_DP, - backgroundShapeCorner, - backgroundShapeCorner, - ) + .background( + shape = shape, + color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.1f), ) - .background(color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.1f)) + .clip(shape) .combinedClickable( onLongClick = if (onEdit == null) null else { { onEdit(data) } }, onClick = { onShowAllArticles(data) }, ) - .padding(start = 20.dp, end = 8.dp) - .padding(vertical = 2.dp), + .padding(start = 20.dp, end = 8.dp, top = 2.dp, bottom = 2.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( @@ -94,15 +73,12 @@ fun Group1Item( ) val expandIconRotate by animateFloatAsState( - targetValue = if (expand) 0f else 180f, + targetValue = if (isExpanded) 0f else 180f, label = "expand icon rotate", ) AniVuIconButton( - onClick = { - expand = !expand - onExpandChange(data, expand) - }, + onClick = { onExpandChange(data, !isExpanded) }, imageVector = Icons.Outlined.KeyboardArrowUp, contentDescription = null, rotate = expandIconRotate, diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/search/SearchScreen.kt b/app/src/main/java/com/skyd/anivu/ui/screen/search/SearchScreen.kt index af05819d..0e3f418f 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/search/SearchScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/search/SearchScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -68,21 +69,19 @@ import com.skyd.anivu.base.mvi.MviEventListener import com.skyd.anivu.base.mvi.getDispatcher import com.skyd.anivu.ext.navigate import com.skyd.anivu.ext.plus -import com.skyd.anivu.model.bean.article.ArticleWithFeed import com.skyd.anivu.model.bean.FeedViewBean +import com.skyd.anivu.model.bean.article.ArticleWithFeed import com.skyd.anivu.ui.component.AniVuFloatingActionButton import com.skyd.anivu.ui.component.AniVuIconButton import com.skyd.anivu.ui.component.BackIcon import com.skyd.anivu.ui.component.CircularProgressPlaceholder import com.skyd.anivu.ui.component.EmptyPlaceholder import com.skyd.anivu.ui.component.dialog.WaitingDialog -import com.skyd.anivu.ui.component.lazyverticalgrid.AniVuLazyVerticalGrid -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy.Article1Proxy -import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy.Feed1Proxy import com.skyd.anivu.ui.local.LocalSearchItemMinWidth import com.skyd.anivu.ui.local.LocalSearchListTonalElevation import com.skyd.anivu.ui.local.LocalSearchTopBarTonalElevation +import com.skyd.anivu.ui.screen.article.Article1Item +import com.skyd.anivu.ui.screen.feed.item.Feed1Item import kotlinx.coroutines.launch import java.io.Serializable @@ -265,31 +264,31 @@ private fun SearchResultList( onRead: (ArticleWithFeed, Boolean) -> Unit, contentPadding: PaddingValues, ) { - val adapter = remember { - LazyGridAdapter( - mutableListOf( - Feed1Proxy(), - Article1Proxy(onFavorite = onFavorite, onRead = onRead), - ) - ) - } - AniVuLazyVerticalGrid( + LazyVerticalGrid( modifier = modifier, columns = GridCells.Adaptive(LocalSearchItemMinWidth.current.dp), - dataList = result, - listState = listState, - adapter = adapter, + state = listState, contentPadding = contentPadding + PaddingValues(horizontal = 12.dp, vertical = 6.dp), verticalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), - key = { _, item -> - when (item) { - is ArticleWithFeed -> item.articleWithEnclosure.article.articleId - is FeedViewBean -> item.feed.url - else -> item.hashCode() + ) { + items( + count = result.itemCount, + key = { index -> + when (val item = result[index]) { + is ArticleWithFeed -> item.articleWithEnclosure.article.articleId + is FeedViewBean -> item.feed.url + else -> item.hashCode() + } + }, + ) { index -> + when (val item = result[index]) { + is FeedViewBean -> Feed1Item(item) + is ArticleWithFeed -> Article1Item(item, onFavorite = onFavorite, onRead = onRead) + else -> Unit } - }, - ) + } + } } @Composable diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/settings/appearance/feed/FeedStyleScreen.kt b/app/src/main/java/com/skyd/anivu/ui/screen/settings/appearance/feed/FeedStyleScreen.kt index 4c60ac06..b7d5e451 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/settings/appearance/feed/FeedStyleScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/settings/appearance/feed/FeedStyleScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Expand import androidx.compose.material.icons.outlined.Restore import androidx.compose.material.icons.outlined.Tonality import androidx.compose.material3.Icon @@ -29,7 +28,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.skyd.anivu.R -import com.skyd.anivu.model.preference.appearance.feed.FeedGroupExpandPreference import com.skyd.anivu.model.preference.appearance.feed.FeedListTonalElevationPreference import com.skyd.anivu.model.preference.appearance.feed.FeedTopBarTonalElevationPreference import com.skyd.anivu.model.preference.appearance.feed.TonalElevationPreferenceUtil @@ -38,9 +36,7 @@ import com.skyd.anivu.ui.component.AniVuTopBar import com.skyd.anivu.ui.component.AniVuTopBarStyle import com.skyd.anivu.ui.component.BaseSettingsItem import com.skyd.anivu.ui.component.CategorySettingsItem -import com.skyd.anivu.ui.component.SwitchSettingsItem import com.skyd.anivu.ui.component.dialog.SliderDialog -import com.skyd.anivu.ui.local.LocalFeedGroupExpand import com.skyd.anivu.ui.local.LocalFeedListTonalElevation import com.skyd.anivu.ui.local.LocalFeedTopBarTonalElevation @@ -87,21 +83,6 @@ fun FeedStyleScreen() { item { CategorySettingsItem(text = stringResource(id = R.string.feed_style_screen_group_list_category)) } - item { - val feedGroupExpand = LocalFeedGroupExpand.current - SwitchSettingsItem( - imageVector = Icons.Outlined.Expand, - text = stringResource(id = R.string.feed_style_screen_group_expand), - checked = feedGroupExpand, - onCheckedChange = { - FeedGroupExpandPreference.put( - context = context, - scope = scope, - value = it, - ) - } - ) - } item { BaseSettingsItem( icon = rememberVectorPainter(Icons.Outlined.Tonality), diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e210fb52..4df2c0f6 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -184,7 +184,6 @@ Beslemeyi ve içerdiği makaleleri sil Başlık Besleme ekranı - Her zaman genişlet Grup listesi Genel stil Ekran stili diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f93b4370..24258167 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -177,7 +177,6 @@ 删除当前订阅及其所有文章 标题 订阅页面 - 总是展开 分组列表 通用样式 页面样式 @@ -335,6 +334,7 @@ 进度指示 文字大小 + 未授权通知权限,无法下载 已读 %d 项 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index d77be536..40ff0953 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -176,7 +176,6 @@ 刪除訂閱和其包含的文章 標題 訂閱頁面 - 總是展開 分組列表 樣式 畫面風格 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 03df39b0..76ceed75 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -184,7 +184,6 @@ Delete the feed and articles it contains Title Feed screen - Always expand Group list Common style Screen style @@ -342,6 +341,7 @@ Value Progress indicator Text size + Notification permission has not been granted so the download cannot work Read %d item Read %d items