diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7869273c..42f18a1c 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-rc01" + versionName = "2.1-rc02" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 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 8e82f0ee..6426d105 100644 --- a/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt @@ -19,6 +19,8 @@ import com.skyd.anivu.model.preference.appearance.feed.FeedDefaultGroupExpandPre import com.skyd.anivu.model.preference.appearance.feed.FeedListTonalElevationPreference import com.skyd.anivu.model.preference.appearance.feed.FeedNumberBadgePreference import com.skyd.anivu.model.preference.appearance.feed.FeedTopBarTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.media.MediaFileFilterPreference +import com.skyd.anivu.model.preference.appearance.media.MediaShowGroupTabPreference import com.skyd.anivu.model.preference.appearance.media.MediaShowThumbnailPreference import com.skyd.anivu.model.preference.appearance.read.ReadContentTonalElevationPreference import com.skyd.anivu.model.preference.appearance.read.ReadTextSizePreference @@ -85,6 +87,7 @@ fun Preferences.toSettings(): Settings { articleItemMinWidth = ArticleItemMinWidthPreference.fromPreferences(this), searchItemMinWidth = SearchItemMinWidthPreference.fromPreferences(this), mediaShowThumbnail = MediaShowThumbnailPreference.fromPreferences(this), + mediaShowGroupTab = MediaShowGroupTabPreference.fromPreferences(this), readTextSize = ReadTextSizePreference.fromPreferences(this), readContentTonalElevation = ReadContentTonalElevationPreference.fromPreferences(this), readTopBarTonalElevation = ReadTopBarTonalElevationPreference.fromPreferences(this), @@ -100,6 +103,7 @@ fun Preferences.toSettings(): Settings { articleSwipeRightAction = ArticleSwipeRightActionPreference.fromPreferences(this), hideEmptyDefault = HideEmptyDefaultPreference.fromPreferences(this), pickImageMethod = PickImageMethodPreference.fromPreferences(this), + mediaFileFilter = MediaFileFilterPreference.fromPreferences(this), // RSS rssSyncFrequency = RssSyncFrequencyPreference.fromPreferences(this), 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 275dad29..881d38d5 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 @@ -24,6 +24,8 @@ import com.skyd.anivu.model.preference.appearance.feed.FeedDefaultGroupExpandPre import com.skyd.anivu.model.preference.appearance.feed.FeedListTonalElevationPreference import com.skyd.anivu.model.preference.appearance.feed.FeedNumberBadgePreference import com.skyd.anivu.model.preference.appearance.feed.FeedTopBarTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.media.MediaFileFilterPreference +import com.skyd.anivu.model.preference.appearance.media.MediaShowGroupTabPreference import com.skyd.anivu.model.preference.appearance.media.MediaShowThumbnailPreference import com.skyd.anivu.model.preference.appearance.read.ReadContentTonalElevationPreference import com.skyd.anivu.model.preference.appearance.read.ReadTextSizePreference @@ -90,7 +92,9 @@ import com.skyd.anivu.ui.local.LocalFeedTopBarTonalElevation import com.skyd.anivu.ui.local.LocalHardwareDecode import com.skyd.anivu.ui.local.LocalHideEmptyDefault import com.skyd.anivu.ui.local.LocalIgnoreUpdateVersion +import com.skyd.anivu.ui.local.LocalMediaFileFilter import com.skyd.anivu.ui.local.LocalMediaLibLocation +import com.skyd.anivu.ui.local.LocalMediaShowGroupTab import com.skyd.anivu.ui.local.LocalMediaShowThumbnail import com.skyd.anivu.ui.local.LocalNavigationBarLabel import com.skyd.anivu.ui.local.LocalOpmlExportDir @@ -151,6 +155,7 @@ data class Settings( val articleItemMinWidth: Float = ArticleItemMinWidthPreference.default, val searchItemMinWidth: Float = SearchItemMinWidthPreference.default, val mediaShowThumbnail: Boolean = MediaShowThumbnailPreference.default, + val mediaShowGroupTab: Boolean = MediaShowGroupTabPreference.default, val readTextSize: Float = ReadTextSizePreference.default, val readContentTonalElevation: Float = ReadContentTonalElevationPreference.default, val readTopBarTonalElevation: Float = ReadTopBarTonalElevationPreference.default, @@ -164,6 +169,7 @@ data class Settings( val articleSwipeRightAction: String = ArticleSwipeRightActionPreference.default, val hideEmptyDefault: Boolean = HideEmptyDefaultPreference.default, val pickImageMethod: String = PickImageMethodPreference.default, + val mediaFileFilter: String = MediaFileFilterPreference.default, // RSS val rssSyncFrequency: Long = RssSyncFrequencyPreference.default, val rssSyncWifiConstraint: Boolean = RssSyncWifiConstraintPreference.default, @@ -229,6 +235,7 @@ fun SettingsProvider( LocalArticleItemMinWidth provides settings.articleItemMinWidth, LocalSearchItemMinWidth provides settings.searchItemMinWidth, LocalMediaShowThumbnail provides settings.mediaShowThumbnail, + LocalMediaShowGroupTab provides settings.mediaShowGroupTab, LocalReadTextSize provides settings.readTextSize, LocalReadContentTonalElevation provides settings.readContentTonalElevation, LocalReadTopBarTonalElevation provides settings.readTopBarTonalElevation, @@ -242,6 +249,7 @@ fun SettingsProvider( LocalArticleSwipeRightAction provides settings.articleSwipeRightAction, LocalHideEmptyDefault provides settings.hideEmptyDefault, LocalPickImageMethod provides settings.pickImageMethod, + LocalMediaFileFilter provides settings.mediaFileFilter, // rss LocalRssSyncFrequency provides settings.rssSyncFrequency, LocalRssSyncWifiConstraint provides settings.rssSyncWifiConstraint, diff --git a/app/src/main/java/com/skyd/anivu/model/preference/appearance/media/MediaFileFilterPreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/appearance/media/MediaFileFilterPreference.kt new file mode 100644 index 00000000..3700130f --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/preference/appearance/media/MediaFileFilterPreference.kt @@ -0,0 +1,47 @@ +package com.skyd.anivu.model.preference.appearance.media + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringPreferencesKey +import com.skyd.anivu.R +import com.skyd.anivu.base.BasePreference +import com.skyd.anivu.ext.dataStore +import com.skyd.anivu.ext.getOrDefault +import com.skyd.anivu.ext.put +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +object MediaFileFilterPreference : BasePreference { + private const val MEDIA_FILE_FILTER = "mediaFileFilter" + + const val VIDEO_REGEX = ".*\\.(mp4|avi|mkv|mov|flv|wmv|webm|mpg|mpeg|3gp|rmvb|ts|mov|m3u8)\$" + const val AUDIO_REGEX = ".*\\.(mp3|wav|flac|aac|ogg|m4a|wma|opus|alac|aiff|aif)\$" + const val MEDIA_REGEX = "($VIDEO_REGEX)|($AUDIO_REGEX)" + const val ALL_REGEX = ".*" + + val values = arrayOf(ALL_REGEX, MEDIA_REGEX, VIDEO_REGEX, AUDIO_REGEX) + + override val default = ALL_REGEX + + val key = stringPreferencesKey(MEDIA_FILE_FILTER) + + fun put(context: Context, scope: CoroutineScope, value: String) { + scope.launch(Dispatchers.IO) { + context.dataStore.put(key, value) + } + } + + override fun fromPreferences(preferences: Preferences): String = preferences[key] ?: default + + fun toDisplayName( + context: Context, + value: String = context.dataStore.getOrDefault(this), + ): String = when (value) { + VIDEO_REGEX -> context.getString(R.string.media_display_filter_video) + AUDIO_REGEX -> context.getString(R.string.media_display_filter_audio) + MEDIA_REGEX -> context.getString(R.string.media_display_filter_media) + ALL_REGEX -> context.getString(R.string.media_display_filter_all) + else -> value + } +} diff --git a/app/src/main/java/com/skyd/anivu/model/preference/appearance/media/MediaShowGroupTabPreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/appearance/media/MediaShowGroupTabPreference.kt new file mode 100644 index 00000000..fd30a1ce --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/preference/appearance/media/MediaShowGroupTabPreference.kt @@ -0,0 +1,26 @@ +package com.skyd.anivu.model.preference.appearance.media + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import com.skyd.anivu.base.BasePreference +import com.skyd.anivu.ext.dataStore +import com.skyd.anivu.ext.put +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +object MediaShowGroupTabPreference : BasePreference { + private const val MEDIA_SHOW_GROUP_TAB = "mediaShowGroupTab" + override val default = true + + val key = booleanPreferencesKey(MEDIA_SHOW_GROUP_TAB) + + fun put(context: Context, scope: CoroutineScope, value: Boolean) { + scope.launch(Dispatchers.IO) { + context.dataStore.put(key, value) + } + } + + override fun fromPreferences(preferences: Preferences): Boolean = preferences[key] ?: default +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/repository/MediaRepository.kt b/app/src/main/java/com/skyd/anivu/model/repository/MediaRepository.kt index c31eebde..f593b88d 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/MediaRepository.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/MediaRepository.kt @@ -2,16 +2,23 @@ package com.skyd.anivu.model.repository import androidx.collection.LruCache import androidx.compose.ui.util.fastFirstOrNull +import com.skyd.anivu.appContext import com.skyd.anivu.base.BaseRepository +import com.skyd.anivu.ext.dataStore import com.skyd.anivu.ext.validateFileName import com.skyd.anivu.model.bean.MediaBean import com.skyd.anivu.model.bean.MediaGroupBean import com.skyd.anivu.model.bean.MediaGroupBean.Companion.isDefaultGroup +import com.skyd.anivu.model.preference.appearance.media.MediaFileFilterPreference import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.serialization.EncodeDefault import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -111,32 +118,45 @@ class MediaRepository @Inject constructor( } } - fun requestFiles(path: String, group: MediaGroupBean?): Flow> { - return flow { - val fileJsons = getOrReadMediaLibJson(path).files.appendFiles( - File(path).listFiles().orEmpty().toMutableList().filter { it.exists() } - ) - val videoList = (if (group == null) fileJsons else { - val groupName = if (group.isDefaultGroup()) null else group.name - fileJsons.filter { it.groupName == groupName } - }).mapNotNull { - val file = File(path, it.fileName) - if (file.exists()) { - MediaBean( - displayName = it.displayName, - file = file, - ) - } else null - } + private val refreshFiles = MutableStateFlow(0) + + fun refreshFile(): Flow = flow { + refreshFiles.emit((refreshFiles.value + 1) % 100) + emit(Unit) + } - emit( + fun requestFiles(path: String, group: MediaGroupBean?): Flow> { + return combine( + refreshFiles.map { + val fileJsons = getOrReadMediaLibJson(path).files.appendFiles( + File(path).listFiles().orEmpty().toMutableList().filter { it.exists() } + ) + val videoList = (if (group == null) fileJsons else { + val groupName = if (group.isDefaultGroup()) null else group.name + fileJsons.filter { it.groupName == groupName } + }).mapNotNull { + val file = File(path, it.fileName) + if (file.exists()) { + MediaBean( + displayName = it.displayName, + file = file, + ) + } else null + } videoList.toMutableList().apply { fastFirstOrNull { it.name.equals(FOLDER_INFO_JSON_NAME, true) } ?.let { remove(it) } fastFirstOrNull { it.name.equals(MEDIA_LIB_JSON_NAME, true) } ?.let { remove(it) } } - ) + }, + appContext.dataStore.data.map { + it[MediaFileFilterPreference.key] ?: MediaFileFilterPreference.default + }.distinctUntilChanged() + ) { videoList, displayFilter -> + videoList.filter { + runCatching { it.file.name.matches(Regex(displayFilter)) }.getOrNull() == true + } } } @@ -158,7 +178,8 @@ class MediaRepository @Inject constructor( val validateFileName = newName.validateFileName() val newFile = File(file.parentFile, validateFileName) if (file.renameTo(newFile)) { - mediaLibJson.files.firstOrNull { it.fileName == file.name }?.fileName = validateFileName + mediaLibJson.files.firstOrNull { it.fileName == file.name }?.fileName = + validateFileName writeMediaLibJson(path = path, mediaLibJson) emit(newFile) } else { 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 b9f22e01..73d6de3a 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 @@ -20,6 +20,8 @@ import com.skyd.anivu.model.preference.appearance.feed.FeedDefaultGroupExpandPre import com.skyd.anivu.model.preference.appearance.feed.FeedListTonalElevationPreference import com.skyd.anivu.model.preference.appearance.feed.FeedNumberBadgePreference import com.skyd.anivu.model.preference.appearance.feed.FeedTopBarTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.media.MediaFileFilterPreference +import com.skyd.anivu.model.preference.appearance.media.MediaShowGroupTabPreference import com.skyd.anivu.model.preference.appearance.media.MediaShowThumbnailPreference import com.skyd.anivu.model.preference.appearance.read.ReadContentTonalElevationPreference import com.skyd.anivu.model.preference.appearance.read.ReadTextSizePreference @@ -99,6 +101,7 @@ val LocalShowArticlePullRefresh = compositionLocalOf { ShowArticlePullRefreshPre val LocalArticleItemMinWidth = compositionLocalOf { ArticleItemMinWidthPreference.default } val LocalSearchItemMinWidth = compositionLocalOf { SearchItemMinWidthPreference.default } val LocalMediaShowThumbnail = compositionLocalOf { MediaShowThumbnailPreference.default } +val LocalMediaShowGroupTab = compositionLocalOf { MediaShowGroupTabPreference.default } val LocalReadTextSize = compositionLocalOf { ReadTextSizePreference.default } val LocalReadContentTonalElevation = compositionLocalOf { ReadContentTonalElevationPreference.default } @@ -116,6 +119,7 @@ val LocalArticleSwipeLeftAction = compositionLocalOf { ArticleSwipeLeftActionPre val LocalArticleSwipeRightAction = compositionLocalOf { ArticleSwipeRightActionPreference.default } val LocalHideEmptyDefault = compositionLocalOf { HideEmptyDefaultPreference.default } val LocalPickImageMethod = compositionLocalOf { PickImageMethodPreference.default } +val LocalMediaFileFilter = compositionLocalOf { MediaFileFilterPreference.default } // RSS val LocalRssSyncFrequency = compositionLocalOf { RssSyncFrequencyPreference.default } diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/media/MediaScreen.kt b/app/src/main/java/com/skyd/anivu/ui/screen/media/MediaScreen.kt index 6d922e02..75aa97de 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/media/MediaScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/media/MediaScreen.kt @@ -17,9 +17,13 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.FileOpen +import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MyLocation +import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Workspaces +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -63,12 +67,14 @@ import com.skyd.anivu.ui.component.AniVuTopBar import com.skyd.anivu.ui.component.AniVuTopBarStyle import com.skyd.anivu.ui.component.dialog.TextFieldDialog import com.skyd.anivu.ui.component.dialog.WaitingDialog +import com.skyd.anivu.ui.local.LocalMediaShowGroupTab import com.skyd.anivu.ui.local.LocalNavController import com.skyd.anivu.ui.local.LocalWindowSizeClass import com.skyd.anivu.ui.screen.filepicker.ListenToFilePicker import com.skyd.anivu.ui.screen.filepicker.openFilePicker import com.skyd.anivu.ui.screen.media.list.GroupInfo import com.skyd.anivu.ui.screen.media.list.MediaList +import com.skyd.anivu.ui.screen.settings.appearance.media.MEDIA_STYLE_SCREEN_ROUTE import kotlinx.coroutines.launch import java.io.File import kotlin.math.min @@ -92,6 +98,7 @@ fun MediaScreen(path: String, viewModel: MediaViewModel = hiltViewModel()) { val pagerState = rememberPagerState(pageCount = { uiState.groups.size }) var openEditGroupDialog by rememberSaveable { mutableStateOf(value = null) } + var openMoreMenu by rememberSaveable { mutableStateOf(false) } ListenToFilePicker { result -> if (result.pickFolder) { @@ -130,6 +137,12 @@ fun MediaScreen(path: String, viewModel: MediaViewModel = hiltViewModel()) { imageVector = Icons.Outlined.Refresh, contentDescription = stringResource(id = R.string.refresh), ) + AniVuIconButton( + onClick = { openMoreMenu = true }, + imageVector = Icons.Outlined.MoreVert, + contentDescription = stringResource(R.string.more), + ) + MoreMenu(expanded = openMoreMenu, onDismissRequest = { openMoreMenu = false }) } ) }, @@ -189,29 +202,31 @@ fun MediaScreen(path: String, viewModel: MediaViewModel = hiltViewModel()) { .padding(innerPadding), ) { if (uiState.groups.isNotEmpty()) { - PrimaryScrollableTabRow( - modifier = Modifier.fillMaxWidth(), - selectedTabIndex = min(uiState.groups.size - 1, pagerState.currentPage), - edgePadding = 0.dp, - divider = {}, - ) { - uiState.groups.forEachIndexed { index, group -> - Tab( - selected = pagerState.currentPage == index, - onClick = { scope.launch { pagerState.animateScrollToPage(index) } }, - text = { - Text( - modifier = Modifier - .widthIn(max = 220.dp) - .basicMarquee(iterations = Int.MAX_VALUE), - text = group.first.name, - maxLines = 1, - ) - }, - ) + if (LocalMediaShowGroupTab.current) { + PrimaryScrollableTabRow( + modifier = Modifier.fillMaxWidth(), + selectedTabIndex = min(uiState.groups.size - 1, pagerState.currentPage), + edgePadding = 0.dp, + divider = {}, + ) { + uiState.groups.forEachIndexed { index, group -> + Tab( + selected = pagerState.currentPage == index, + onClick = { scope.launch { pagerState.animateScrollToPage(index) } }, + text = { + Text( + modifier = Modifier + .widthIn(max = 220.dp) + .basicMarquee(iterations = Int.MAX_VALUE), + text = group.first.name, + maxLines = 1, + ) + }, + ) + } } + HorizontalDivider() } - HorizontalDivider() HorizontalPager(state = pagerState) { index -> MediaList( @@ -318,4 +333,24 @@ internal fun CreateGroupDialog( onConfirm = { text -> onCreateGroup(MediaGroupBean(name = text)) }, onDismissRequest = onDismissRequest, ) +} + +@Composable +private fun MoreMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, +) { + val navController = LocalNavController.current + DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) { + DropdownMenuItem( + text = { Text(text = stringResource(R.string.media_screen_style)) }, + leadingIcon = { + Icon(imageVector = Icons.Outlined.Palette, contentDescription = null) + }, + onClick = { + onDismissRequest() + navController.navigate(MEDIA_STYLE_SCREEN_ROUTE) + }, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/media/list/Media1Item.kt b/app/src/main/java/com/skyd/anivu/ui/screen/media/list/Media1Item.kt index 73e2875c..93d8d3f2 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/media/list/Media1Item.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/media/list/Media1Item.kt @@ -165,7 +165,8 @@ fun Media1Item( Text( modifier = Modifier .basicMarquee() - .widthIn(min = 40.dp), + .widthIn(min = 40.dp) + .alignByBaseline(), text = remember(data) { data.size.fileSize(context) }, style = MaterialTheme.typography.labelMedium, maxLines = 1, @@ -174,8 +175,10 @@ fun Media1Item( Spacer(modifier = Modifier.weight(1f)) Text( modifier = Modifier + .padding(start = 12.dp) .basicMarquee() - .widthIn(min = 40.dp), + .widthIn(min = 40.dp) + .alignByBaseline(), text = remember(data) { data.date.toDateTimeString(context = context) }, style = MaterialTheme.typography.labelMedium, maxLines = 1, diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaListIntent.kt b/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaListIntent.kt index 8f9080bb..012e4d9f 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaListIntent.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaListIntent.kt @@ -6,7 +6,7 @@ import com.skyd.anivu.model.bean.MediaGroupBean import java.io.File sealed interface MediaListIntent : MviIntent { - data class Init(val path: String?, val group: MediaGroupBean?, val version: Long?) : + data class Init(val path: String, val group: MediaGroupBean?, val version: Long?) : MediaListIntent data class Refresh(val path: String?, val group: MediaGroupBean?) : MediaListIntent diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaListPartialStateChange.kt b/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaListPartialStateChange.kt index 11863ca7..02e0f2be 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaListPartialStateChange.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaListPartialStateChange.kt @@ -76,6 +76,19 @@ internal sealed interface MediaListPartialStateChange { data class Failed(val msg: String) : DeleteFileResult } + sealed interface RefreshFilesResult : MediaListPartialStateChange { + override fun reduce(oldState: MediaListState): MediaListState { + return when (this) { + is Success, is Failed -> oldState.copy( + loadingDialog = false, + ) + } + } + + data object Success : RefreshFilesResult + data class Failed(val msg: String) : RefreshFilesResult + } + sealed interface RenameFileResult : MediaListPartialStateChange { override fun reduce(oldState: MediaListState): MediaListState { return when (this) { diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaListViewModel.kt b/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaListViewModel.kt index b65ee622..3788f0ca 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaListViewModel.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaListViewModel.kt @@ -57,25 +57,23 @@ class MediaListViewModel @Inject constructor( private fun Flow.toMediaListPartialStateChangeFlow(): Flow { return merge( - merge( - filterIsInstance().filterNot { it.path.isNullOrBlank() }, - filterIsInstance().filterNot { it.path.isNullOrBlank() }, - ).flatMapConcat { intent -> - val path = if (intent is MediaListIntent.Init) intent.path - else (intent as MediaListIntent.Refresh).path - val group = if (intent is MediaListIntent.Init) intent.group - else (intent as MediaListIntent.Refresh).group + filterIsInstance().flatMapConcat { intent -> combine( - mediaRepo.requestFiles(path = path!!, group), - mediaRepo.requestGroups(path = path), + mediaRepo.requestFiles(path = intent.path, intent.group), + mediaRepo.requestGroups(path = intent.path), ) { files, groups -> MediaListPartialStateChange.MediaListResult.Success( - list = files, - groups = groups + list = files, groups = groups ) }.startWith(MediaListPartialStateChange.LoadingDialog.Show) .catchMap { MediaListPartialStateChange.MediaListResult.Failed(it.message.toString()) } }, + filterIsInstance().flatMapConcat { + mediaRepo.refreshFile().map { + MediaListPartialStateChange.RefreshFilesResult.Success + }.startWith(MediaListPartialStateChange.LoadingDialog.Show) + .catchMap { MediaListPartialStateChange.RefreshFilesResult.Failed(it.message.toString()) } + }, filterIsInstance().flatMapConcat { intent -> mediaRepo.deleteFile(intent.file).map { MediaListPartialStateChange.DeleteFileResult.Success(file = intent.file) diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/settings/appearance/media/MediaStyleScreen.kt b/app/src/main/java/com/skyd/anivu/ui/screen/settings/appearance/media/MediaStyleScreen.kt index da3c3eb0..9c7934e4 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/settings/appearance/media/MediaStyleScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/settings/appearance/media/MediaStyleScreen.kt @@ -3,6 +3,7 @@ package com.skyd.anivu.ui.screen.settings.appearance.media import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Toc import androidx.compose.material.icons.outlined.HideImage import androidx.compose.material.icons.outlined.Image import androidx.compose.material3.Scaffold @@ -15,11 +16,13 @@ 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.media.MediaShowGroupTabPreference import com.skyd.anivu.model.preference.appearance.media.MediaShowThumbnailPreference import com.skyd.anivu.ui.component.AniVuTopBar import com.skyd.anivu.ui.component.AniVuTopBarStyle import com.skyd.anivu.ui.component.CategorySettingsItem import com.skyd.anivu.ui.component.SwitchSettingsItem +import com.skyd.anivu.ui.local.LocalMediaShowGroupTab import com.skyd.anivu.ui.local.LocalMediaShowThumbnail @@ -64,6 +67,21 @@ fun MediaStyleScreen() { } ) } + item { + val mediaShowGroupTab = LocalMediaShowGroupTab.current + SwitchSettingsItem( + imageVector = Icons.AutoMirrored.Outlined.Toc, + text = stringResource(id = R.string.media_style_screen_media_list_show_group_tab), + checked = mediaShowGroupTab, + onCheckedChange = { + MediaShowGroupTabPreference.put( + context = context, + scope = scope, + value = it, + ) + } + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/settings/behavior/BehaviorScreen.kt b/app/src/main/java/com/skyd/anivu/ui/screen/settings/behavior/BehaviorScreen.kt index 476be821..eb783ca0 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/settings/behavior/BehaviorScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/settings/behavior/BehaviorScreen.kt @@ -1,15 +1,24 @@ package com.skyd.anivu.ui.screen.settings.behavior +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow 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.automirrored.outlined.Article +import androidx.compose.material.icons.outlined.FilterAlt import androidx.compose.material.icons.outlined.SwipeLeft import androidx.compose.material.icons.outlined.SwipeRight import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SuggestionChip import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -19,12 +28,15 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.skyd.anivu.R +import com.skyd.anivu.model.preference.appearance.media.MediaFileFilterPreference 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 @@ -35,12 +47,15 @@ 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.CheckableListMenu +import com.skyd.anivu.ui.component.ClipboardTextField import com.skyd.anivu.ui.component.SwitchSettingsItem +import com.skyd.anivu.ui.component.dialog.AniVuDialog 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.LocalHideEmptyDefault +import com.skyd.anivu.ui.local.LocalMediaFileFilter const val BEHAVIOR_SCREEN_ROUTE = "behaviorScreen" @@ -53,6 +68,7 @@ fun BehaviorScreen() { var expandArticleTapActionMenu by rememberSaveable { mutableStateOf(false) } var expandArticleSwipeLeftActionMenu by rememberSaveable { mutableStateOf(false) } var expandArticleSwipeRightActionMenu by rememberSaveable { mutableStateOf(false) } + var openMediaFileFilterDialog by rememberSaveable { mutableStateOf(null) } Scaffold( topBar = { @@ -172,8 +188,35 @@ fun BehaviorScreen() { onClick = { expandArticleSwipeRightActionMenu = true }, ) } + item { + CategorySettingsItem(text = stringResource(id = R.string.behavior_screen_media_screen_category)) + } + item { + val mediaFileFilter = LocalMediaFileFilter.current + BaseSettingsItem( + icon = rememberVectorPainter(image = Icons.Outlined.FilterAlt), + text = stringResource(id = R.string.behavior_screen_media_file_filter), + descriptionText = MediaFileFilterPreference.toDisplayName( + context = context, + value = mediaFileFilter, + ), + onClick = { openMediaFileFilterDialog = mediaFileFilter }, + ) + } } } + + if (openMediaFileFilterDialog != null) { + MediaFileFilterDialog( + onDismissRequest = { openMediaFileFilterDialog = null }, + initValue = openMediaFileFilterDialog!!, + onConfirm = { + MediaFileFilterPreference.put( + context = context, scope = scope, value = it, + ) + } + ) + } } @Composable @@ -209,4 +252,67 @@ private fun ArticleSwipeActionMenu( onChecked = onClick, onDismissRequest = onDismissRequest, ) +} + +@Composable +internal fun MediaFileFilterDialog( + onDismissRequest: () -> Unit, + initValue: String, + onConfirm: (String) -> Unit, +) { + val context = LocalContext.current + var value by rememberSaveable { mutableStateOf(initValue) } + + AniVuDialog( + onDismissRequest = onDismissRequest, + icon = { Icon(Icons.Outlined.FilterAlt, contentDescription = null) }, + title = { Text(stringResource(R.string.behavior_screen_media_file_filter)) }, + text = { + Column { + ClipboardTextField( + modifier = Modifier.fillMaxWidth(), + value = value, + singleLine = true, + onValueChange = { value = it }, + onConfirm = onConfirm, + placeholder = stringResource(R.string.behavior_screen_media_file_filter_placeholder) + ) + FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + MediaFileFilterPreference.values.forEach { filter -> + SuggestionChip( + onClick = { value = filter }, + label = { + Text(MediaFileFilterPreference.toDisplayName(context, filter)) + } + ) + } + } + } + + }, + confirmButton = { + val enabled = value.isNotBlank() && runCatching { Regex(value) }.isSuccess + TextButton( + enabled = enabled, + onClick = { + onConfirm(value) + onDismissRequest() + } + ) { + Text( + text = stringResource(R.string.ok), + color = if (enabled) { + Color.Unspecified + } else { + MaterialTheme.colorScheme.outline.copy(alpha = 0.7f) + } + ) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(R.string.cancel)) + } + }, + ) } \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3993ae59..82bbfe8e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -329,6 +329,7 @@ 媒体页面 媒体列表 显示预览图 + 显示组选项卡 第 %s 集 这将删除所有文章,但不会删除订阅,确定吗? 请求头 @@ -361,6 +362,14 @@ 播放器 播放器通知栏控制 后台播放 + 样式 + 视频 + 音频 + 媒体 + 所有文件 + 媒体库 + 文件过滤 + 文件名正则 导入了 %d 项,花费 %.2f 秒 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 05ef0bee..562b26b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -336,6 +336,7 @@ Media screen Media list Show thumbnails + Show group tab Episode %s This will delete all articles, but not feeds, sure? Request headers @@ -374,6 +375,14 @@ All Number badge + Style + Video + Audio + Media + All files + Media library + File filter + Filename regex Imported %d item, takes %.2f seconds Imported %d items, takes %.2f seconds