From 9881815f7d6a1926ca7762f71496d41315a04f91 Mon Sep 17 00:00:00 2001 From: SkyD666 Date: Sun, 8 Dec 2024 00:14:32 +0800 Subject: [PATCH] [feature|optimize] Support set nickname for files in Media Library root directory; support favorite article in reading page; optimize Media Library implementation; optimize video title display logic --- app/build.gradle.kts | 2 +- .../com/skyd/anivu/model/db/dao/ArticleDao.kt | 9 + .../anivu/model/repository/MediaRepository.kt | 372 ++++++++++-------- .../anivu/model/repository/ReadRepository.kt | 6 +- .../skyd/anivu/ui/activity/PlayActivity.kt | 28 +- .../java/com/skyd/anivu/ui/mpv/PlayerView.kt | 8 +- .../ui/mpv/controller/PlayerController.kt | 2 +- .../ui/mpv/controller/state/PlayState.kt | 2 + .../anivu/ui/screen/article/Article1Item.kt | 3 +- .../article/enclosure/EnclosureBottomSheet.kt | 58 ++- .../skyd/anivu/ui/screen/media/MediaScreen.kt | 5 +- .../anivu/ui/screen/media/MediaViewModel.kt | 2 +- .../ui/screen/media/list/EditMediaSheet.kt | 31 ++ .../anivu/ui/screen/media/list/Media1Item.kt | 6 +- .../anivu/ui/screen/media/list/MediaList.kt | 10 + .../ui/screen/media/list/MediaListIntent.kt | 2 + .../media/list/MediaListPartialStateChange.kt | 39 ++ .../screen/media/list/MediaListViewModel.kt | 12 +- .../ui/screen/read/ReadPartialStateChange.kt | 4 +- .../skyd/anivu/ui/screen/read/ReadScreen.kt | 128 +++--- .../skyd/anivu/ui/screen/read/ReadState.kt | 4 +- .../anivu/ui/screen/read/ReadViewModel.kt | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + gradle/libs.versions.toml | 6 +- 25 files changed, 472 insertions(+), 271 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7a0c644d..316b7f48 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-beta14" + versionName = "2.1-beta15" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/com/skyd/anivu/model/db/dao/ArticleDao.kt b/app/src/main/java/com/skyd/anivu/model/db/dao/ArticleDao.kt index ff3ef2c0..d936ea71 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/dao/ArticleDao.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/dao/ArticleDao.kt @@ -166,6 +166,15 @@ interface ArticleDao { ) fun getArticleWithEnclosures(articleId: String): Flow + @Transaction + @Query( + """ + SELECT * FROM $ARTICLE_TABLE_NAME + WHERE ${ArticleBean.ARTICLE_ID_COLUMN} LIKE :articleId + """ + ) + fun getArticleWithFeed(articleId: String): Flow + @RewriteQueriesToDropUnusedColumns @Query( """ 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 1acb688f..a5b7d6eb 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 @@ -1,5 +1,6 @@ package com.skyd.anivu.model.repository +import androidx.collection.LruCache import androidx.compose.ui.util.fastFirstOrNull import com.skyd.anivu.base.BaseRepository import com.skyd.anivu.ext.validateFileName @@ -24,120 +25,185 @@ class MediaRepository @Inject constructor( ) : BaseRepository() { companion object { - const val FOLDER_INFO_JSON_NAME = "info.json" - const val GROUP_JSON_NAME = "group.json" + private const val FOLDER_INFO_JSON_NAME = "info.json" + private const val OLD_MEDIA_LIB_JSON_NAME = "group.json" + private const val MEDIA_LIB_JSON_NAME = "MediaLib.json" + + private val mediaLibJsons = LruCache(maxSize = 5) + } + + private fun parseMediaLibJson(mediaLibRootJsonFile: File): MediaLibJson? { + if (!mediaLibRootJsonFile.exists()) { + File(mediaLibRootJsonFile.parentFile, OLD_MEDIA_LIB_JSON_NAME).apply { + if (this.exists()) renameTo(mediaLibRootJsonFile) + } + } + if (!mediaLibRootJsonFile.exists()) return null + return mediaLibRootJsonFile.inputStream().use { inputStream -> + json.decodeFromStream(inputStream) + }.apply { + files.removeIf { + !File(mediaLibRootJsonFile.parentFile, it.fileName).exists() || + it.fileName.equals(FOLDER_INFO_JSON_NAME, true) || + it.fileName.equals(MEDIA_LIB_JSON_NAME, true) + } + } + } + + private fun getOrReadMediaLibJson(path: String): MediaLibJson { + return mediaLibJsons[path] ?: run { + val jsonFile = File(path, MEDIA_LIB_JSON_NAME) + parseMediaLibJson(jsonFile)?.also { + mediaLibJsons.put(path, it) + } ?: MediaLibJson(files = mutableListOf()).apply { mediaLibJsons.put(path, this) } + } + } + + // Format groups + private fun formatMediaLibJson(old: MediaLibJson): MediaLibJson { + val allGroups = (old.files.map { it.groupName } + old.allGroups) + .distinct().filterNotNull().toMutableList() + return MediaLibJson( + allGroups = allGroups, + files = old.files, + ) + } + + private fun writeMediaLibJson(path: String, data: MediaLibJson) { + File(path, MEDIA_LIB_JSON_NAME).outputStream().use { outputStream -> + json.encodeToStream(formatMediaLibJson(data), outputStream) + } } - fun requestGroups(uriPath: String): Flow> { + private fun MutableList.appendFiles( + files: List, fileJsonBuild: (File) -> FileJson = { + FileJson( + fileName = it.name, + groupName = null, + isFile = it.isFile, + displayName = null, + ) + } + ) = apply { + files.forEach { file -> + if (file.name.equals(FOLDER_INFO_JSON_NAME, true) || + file.name.equals(MEDIA_LIB_JSON_NAME, true) + ) { + return@forEach + } + if (firstOrNull { it.fileName == file.name } == null) { + add(fileJsonBuild(file)) + } + } + } + + fun requestGroups(path: String): Flow> { return flow { - val groupJsonFile = File(uriPath, GROUP_JSON_NAME) - val mediaGroupJson = parseGroupJson(groupJsonFile) - val allGroups = mediaGroupJson?.allGroups.orEmpty() + val allGroups = getOrReadMediaLibJson(path).allGroups emit(listOf(MediaGroupBean.DefaultMediaGroup) + allGroups.map { MediaGroupBean(name = it) }.sortedBy { it.name }) - }.flowOn(Dispatchers.IO) + } } - fun requestFiles(uriPath: String, group: MediaGroupBean?): Flow> { + fun requestFiles(path: String, group: MediaGroupBean?): Flow> { return flow { - val groupJsonFile = File(uriPath, GROUP_JSON_NAME) - val mediaGroupJson = parseGroupJson(groupJsonFile) - val filesInGroup = mediaGroupJson?.files.orEmpty() - val allFiles = - File(uriPath).listFiles().orEmpty().toMutableList().filter { it.exists() } - val videoList = if (group == null) { - allFiles.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 = null, // TODO - file = it, + displayName = it.displayName, + file = file, ) - } - } else { - if (group.isDefaultGroup()) { - allFiles.filter { file -> - filesInGroup.fastFirstOrNull { it.fileName == file.name } == null - }.map { file -> - MediaBean( - displayName = null, // TODO - file = file, - ) - } - } else { - filesInGroup.filter { it.groupName == group.name }.mapNotNull { - val file = File(uriPath, it.fileName) - if (file.exists()) { - MediaBean( - displayName = null, // TODO - file = file, - ) - } else null - } - } - + } else null } - emit(videoList.toMutableList().apply { - fastFirstOrNull { it.name.equals(FOLDER_INFO_JSON_NAME, true) }?.let { remove(it) } - fastFirstOrNull { it.name.equals(GROUP_JSON_NAME, true) }?.let { remove(it) } - }) - }.flowOn(Dispatchers.IO) + + emit( + 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) } + } + ) + } } fun deleteFile(file: File): Flow { return flow { + val path = file.parentFile!!.path + val mediaLibJson = getOrReadMediaLibJson(path).apply { + files.removeIf { it.fileName == file.name } + } + writeMediaLibJson(path = path, mediaLibJson) emit(file.deleteRecursively()) }.flowOn(Dispatchers.IO) } fun renameFile(file: File, newName: String): Flow { return flow { + val path = file.parentFile!!.path + val mediaLibJson = getOrReadMediaLibJson(path) val newFile = File(file.parentFile, newName.validateFileName()) - emit(if (file.renameTo(newFile)) newFile else null) + if (file.renameTo(newFile)) { + mediaLibJson.files.firstOrNull { it.fileName == file.name }?.fileName = newName + writeMediaLibJson(path = path, mediaLibJson) + emit(newFile) + } else { + emit(null) + } }.flowOn(Dispatchers.IO) } - fun createGroup(uriPath: String, group: MediaGroupBean): Flow = flow { + fun setFileDisplayName(mediaBean: MediaBean, displayName: String?): Flow { + return flow { + val path = mediaBean.file.parentFile!!.path + val mediaLibJson = getOrReadMediaLibJson(path = path) + mediaLibJson.files.firstOrNull { + it.fileName == mediaBean.file.name + }?.displayName = if (displayName.isNullOrBlank()) null else displayName + writeMediaLibJson(path = path, mediaLibJson) + + emit(mediaBean.copy(displayName = displayName)) + }.flowOn(Dispatchers.IO) + } + + fun createGroup(path: String, group: MediaGroupBean): Flow = flow { if (group.isDefaultGroup()) { emit(Unit) return@flow } - val groupJsonFile = File(uriPath, GROUP_JSON_NAME) - val mediaGroupJson = parseGroupJson(groupJsonFile) ?: MediaGroupJson(files = emptyList()) - - writeGroupToJson( - groupJsonFile, - mediaGroupJson.copy(allGroups = mediaGroupJson.allGroups + group.name), - ) + val mediaLibJson = getOrReadMediaLibJson(path = path) + mediaLibJson.allGroups.add(group.name) + writeMediaLibJson(path = path, mediaLibJson) emit(Unit) }.flowOn(Dispatchers.IO) - fun deleteGroup(uriPath: String, group: MediaGroupBean): Flow = flow { + fun deleteGroup(path: String, group: MediaGroupBean): Flow = flow { if (group.isDefaultGroup()) { - emit(0) + emit(Unit) return@flow } - val groupJsonFile = File(uriPath, GROUP_JSON_NAME) - val mediaGroupJson = parseGroupJson(groupJsonFile)!! - val fileToGroups = mediaGroupJson.files.toSet() - val newFileToGroups = fileToGroups.filter { it.groupName != group.name }.toSet() - - writeGroupToJson( - groupJsonFile, - mediaGroupJson.copy( - allGroups = mediaGroupJson.allGroups - group.name, - files = newFileToGroups.toList() - ), - ) - - (fileToGroups - newFileToGroups).forEach { - File(uriPath, it.fileName).deleteRecursively() + val mediaLibJson = getOrReadMediaLibJson(path = path) + mediaLibJson.files.forEach { + if (it.groupName == group.name) { + it.groupName = null + } } + mediaLibJson.allGroups.remove(group.name) + writeMediaLibJson(path = path, mediaLibJson) - emit(newFileToGroups.count() - fileToGroups.count()) + emit(Unit) }.flowOn(Dispatchers.IO) fun renameGroup( - uriPath: String, + path: String, group: MediaGroupBean, newName: String, ): Flow = flow { @@ -145,55 +211,44 @@ class MediaRepository @Inject constructor( emit(MediaGroupBean.DefaultMediaGroup) return@flow } - val groupJsonFile = File(uriPath, GROUP_JSON_NAME) - val mediaGroupJson = parseGroupJson(groupJsonFile)!! - - writeGroupToJson( - groupJsonFile, - mediaGroupJson.copy( - allGroups = mediaGroupJson.allGroups.toMutableList().apply { - val index = indexOf(group.name) - if (index != -1 && index < size) { - set(index, newName) - } - }, - files = mediaGroupJson.files.map { - if (it.groupName == group.name) { - it.copy(groupName = newName) - } else { - it - } - } - ), - ) + val mediaLibJson = getOrReadMediaLibJson(path = path) + mediaLibJson.files.forEach { + if (it.groupName == group.name) { + it.groupName = newName + } + } + val index = mediaLibJson.allGroups.indexOf(group.name) + if (index >= 0) { + mediaLibJson.allGroups[index] = newName + } + writeMediaLibJson(path = path, mediaLibJson) emit(MediaGroupBean(name = newName)) }.flowOn(Dispatchers.IO) fun changeMediaGroup( - uriPath: String, + path: String, mediaBean: MediaBean, group: MediaGroupBean, ): Flow = flow { - val groupJsonFile = File(uriPath, GROUP_JSON_NAME) - val mediaGroupJson = parseGroupJson(groupJsonFile)!! - val fileToGroups = mediaGroupJson.files.toSet() - val file = fileToGroups.firstOrNull { it.fileName == mediaBean.file.name } - - val newFileToGroups = (if (file != null) fileToGroups - file else fileToGroups) - .toMutableList() - .apply { - if (!group.isDefaultGroup()) { - add( - FileToGroup( - fileName = mediaBean.file.name, - groupName = group.name, - isFile = mediaBean.file.isFile, - ) + val mediaLibJson = getOrReadMediaLibJson(path = path) + val index = mediaLibJson.files.indexOfFirst { it.fileName == mediaBean.file.name } + if (index >= 0) { + mediaLibJson.files[index].groupName = + if (group.isDefaultGroup()) null else group.name + } else { + if (!group.isDefaultGroup()) { + mediaLibJson.files.add( + FileJson( + fileName = mediaBean.file.name, + groupName = group.name, + isFile = mediaBean.file.isFile, + displayName = mediaBean.displayName, ) - } + ) } - writeGroupToJson(groupJsonFile, mediaGroupJson.copy(files = newFileToGroups.toList())) + } + writeMediaLibJson(path = path, mediaLibJson) emit(Unit) }.flowOn(Dispatchers.IO) @@ -203,56 +258,58 @@ class MediaRepository @Inject constructor( from: MediaGroupBean, to: MediaGroupBean ): Flow = flow { - val groupJsonFile = File(path, GROUP_JSON_NAME) - val mediaGroupJson = parseGroupJson(groupJsonFile)!! - val fileToGroups = mediaGroupJson.files.toSet() - val newFileToGroups: List = if (from.isDefaultGroup()) { + val mediaLibJson = getOrReadMediaLibJson(path = path) + if (from.isDefaultGroup()) { if (to.isDefaultGroup()) { - fileToGroups.toList() + emit(Unit) + return@flow } else { - fileToGroups.toList() + File(path).listFiles().orEmpty().filter { f -> - // null means the current file f is not grouped - fileToGroups.firstOrNull { - f.isFile == it.isFile && f.name == it.fileName - } == null - }.map { FileToGroup(it.name, to.name, isFile = it.isFile) } + mediaLibJson.files.appendFiles( + files = File(path).listFiles().orEmpty().toList(), + fileJsonBuild = { + FileJson( + fileName = it.name, + groupName = to.name, + isFile = it.isFile, + displayName = null, + ) + } + ) + mediaLibJson.files.forEach { + if (it.groupName == null) it.groupName = to.name + } } } else { - if (to.isDefaultGroup()) { - fileToGroups.toList() - fileToGroups.filter { it.groupName == from.name }.toSet() - } else { - fileToGroups.map { - if (it.groupName == from.name) { - it.copy(groupName = to.name) - } else { - it - } + mediaLibJson.files.forEach { + if (it.groupName == from.name) { + it.groupName = if (to.isDefaultGroup()) null else to.name } } } - - writeGroupToJson(groupJsonFile, mediaGroupJson.copy(files = newFileToGroups.toList())) + writeMediaLibJson(path = path, mediaLibJson) emit(Unit) }.flowOn(Dispatchers.IO) @Serializable - data class MediaGroupJson( + data class MediaLibJson( @SerialName("allGroups") @EncodeDefault - val allGroups: List = listOf(), + val allGroups: MutableList = mutableListOf(), @SerialName("files") - val files: List, + val files: MutableList, ) @Serializable - data class FileToGroup( + data class FileJson( @SerialName("fileName") - val fileName: String, + var fileName: String, @SerialName("groupName") - val groupName: String, + var groupName: String? = null, @SerialName("isFile") - val isFile: Boolean = false, + var isFile: Boolean = false, + @SerialName("displayName") + var displayName: String? = null, ) @Serializable @@ -260,37 +317,4 @@ class MediaRepository @Inject constructor( @SerialName("displayName") val displayName: String? = null, ) - - // Format groups - private fun formatMediaGroupJson(old: MediaGroupJson): MediaGroupJson { - val allGroups = (old.files.map { it.groupName } + old.allGroups).distinct() - return MediaGroupJson( - allGroups = allGroups, - files = old.files, - ) - } - - private fun parseGroupJson(groupJsonFile: File): MediaGroupJson? { - if (!groupJsonFile.exists()) return null - return groupJsonFile.inputStream().use { inputStream -> - json.decodeFromStream(inputStream) - } - } - - private fun parseGroupJsonToMap(groupJsonFile: File): Map> { - val mediaGroupJson = parseGroupJson(groupJsonFile) ?: return emptyMap() - return mediaGroupJson.files.groupBy { it.groupName }.toMutableMap().apply { - mediaGroupJson.allGroups.forEach { - if (!containsKey(it)) { - put(it, emptyList()) - } - } - }.toSortedMap() - } - - private fun writeGroupToJson(groupJsonFile: File, data: MediaGroupJson) { - groupJsonFile.outputStream().use { outputStream -> - json.encodeToStream(formatMediaGroupJson(data), outputStream) - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/repository/ReadRepository.kt b/app/src/main/java/com/skyd/anivu/model/repository/ReadRepository.kt index 8c370f96..4f409d87 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/ReadRepository.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/ReadRepository.kt @@ -14,7 +14,7 @@ import com.skyd.anivu.ext.savePictureToMediaStore import com.skyd.anivu.ext.share import com.skyd.anivu.ext.toUri import com.skyd.anivu.ext.validateFileName -import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean +import com.skyd.anivu.model.bean.article.ArticleWithFeed import com.skyd.anivu.model.db.dao.ArticleDao import com.skyd.anivu.util.image.ImageFormatChecker import kotlinx.coroutines.Dispatchers @@ -32,8 +32,8 @@ import kotlin.time.Duration.Companion.milliseconds class ReadRepository @Inject constructor( private val articleDao: ArticleDao, ) : BaseRepository() { - fun requestArticleWithEnclosure(articleId: String): Flow { - return articleDao.getArticleWithEnclosures(articleId = articleId) + fun requestArticleWithFeed(articleId: String): Flow { + return articleDao.getArticleWithFeed(articleId = articleId) .filterNotNull() .flowOn(Dispatchers.IO) } diff --git a/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt b/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt index 55c94d07..120e396c 100644 --- a/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt +++ b/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt @@ -11,7 +11,6 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.core.content.IntentCompat import androidx.core.util.Consumer @@ -27,11 +26,13 @@ import java.io.File class PlayActivity : BaseComposeActivity() { companion object { const val VIDEO_URI_KEY = "videoUri" + const val VIDEO_TITLE_KEY = "videoTitle" - fun play(activity: Activity, uri: Uri) { + fun play(activity: Activity, uri: Uri, title: String? = null) { activity.startActivity( Intent(activity, PlayActivity::class.java).apply { putExtra(VIDEO_URI_KEY, uri) + putExtra(VIDEO_TITLE_KEY, title) } ) } @@ -49,6 +50,9 @@ class PlayActivity : BaseComposeActivity() { } } + private var videoUri by mutableStateOf(null) + private var videoTitle by mutableStateOf(null) + override fun onCreate(savedInstanceState: Bundle?) { copyAssetsForMpv(this) @@ -57,19 +61,18 @@ class PlayActivity : BaseComposeActivity() { // Keep screen on window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - setContentBase { - var videoUri by rememberSaveable { mutableStateOf(handleIntent(intent)) } + handleIntent(intent) + setContentBase { DisposableEffect(Unit) { - val listener = Consumer { newIntent -> - videoUri = handleIntent(newIntent) - } + val listener = Consumer { newIntent -> handleIntent(newIntent) } addOnNewIntentListener(listener) onDispose { removeOnNewIntentListener(listener) } } videoUri?.let { uri -> PlayerView( uri = uri, + title = videoTitle, onBack = { finish() }, onSaveScreenshot = { picture = it @@ -81,10 +84,13 @@ class PlayActivity : BaseComposeActivity() { } } - private fun handleIntent(intent: Intent?): Uri? { - intent ?: return null - return IntentCompat.getParcelableExtra(intent, VIDEO_URI_KEY, Uri::class.java) - ?: intent.data + private fun handleIntent(intent: Intent?) { + intent ?: return + + videoUri = IntentCompat.getParcelableExtra( + intent, VIDEO_URI_KEY, Uri::class.java + ) ?: intent.data + videoTitle = intent.getStringExtra(VIDEO_TITLE_KEY) } private fun saveScreenshot() { diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt index af93e140..a81f2b99 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt @@ -141,6 +141,7 @@ private fun MPVView.solveCommand( @Composable fun PlayerView( uri: Uri, + title: String? = null, onBack: () -> Unit, onSaveScreenshot: (File) -> Unit, configDir: String = Const.MPV_CONFIG_DIR.path, @@ -173,6 +174,11 @@ fun PlayerView( var subtitleTrackDialogState by remember { mutableStateOf(SubtitleTrackDialogState.initial) } var audioTrackDialogState by remember { mutableStateOf(AudioTrackDialogState.initial) } var speedDialogState by remember { mutableStateOf(SpeedDialogState.initial) } + LaunchedEffect(title) { + if (title != null) { + playState = playState.copy(title = title) + } + } LaunchedEffect(playState.speed) { speedDialogState = speedDialogState.copy(currentSpeed = playState.speed) } @@ -274,7 +280,7 @@ fun PlayerView( override fun eventProperty(property: String, value: String) { when (property) { - "media-title" -> playState = playState.copy(title = value) + "media-title" -> playState = playState.copy(mediaTitle = value) } } diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/PlayerController.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/PlayerController.kt index 56c347de..ff67293e 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/PlayerController.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/PlayerController.kt @@ -314,7 +314,7 @@ private fun AutoHiddenBox( TopBar( modifier = Modifier.constrainAs(topBar) { top.linkTo(parent.top) }, - title = playState().title, + title = playState().run { title.ifBlank { mediaTitle } }, topBarCallback = topBarCallback, ) BottomBar( diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/PlayState.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/PlayState.kt index 44bdc243..517323b7 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/PlayState.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/PlayState.kt @@ -10,6 +10,7 @@ data class PlayState( val bufferDuration: Int, val speed: Float, val title: String, + val mediaTitle: String, ) { companion object { val initial = PlayState( @@ -20,6 +21,7 @@ data class PlayState( bufferDuration = 0, speed = 1f, title = "", + mediaTitle = "", ) } } diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/article/Article1Item.kt b/app/src/main/java/com/skyd/anivu/ui/screen/article/Article1Item.kt index c2310276..92fc159d 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/article/Article1Item.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/article/Article1Item.kt @@ -188,7 +188,8 @@ fun Article1Item( if (openEnclosureBottomSheet != null) { EnclosureBottomSheet( onDismissRequest = { openEnclosureBottomSheet = null }, - dataList = openEnclosureBottomSheet.orEmpty() + dataList = openEnclosureBottomSheet.orEmpty(), + article = data.articleWithEnclosure, ) } } 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 8b74b828..fff6ef8d 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 @@ -37,6 +37,7 @@ import com.skyd.anivu.ext.fileSize import com.skyd.anivu.ext.getOrDefault import com.skyd.anivu.model.bean.LinkEnclosureBean import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean +import com.skyd.anivu.model.bean.article.ArticleWithFeed import com.skyd.anivu.model.bean.article.EnclosureBean import com.skyd.anivu.model.preference.rss.ParseLinkTagAsEnclosurePreference import com.skyd.anivu.model.repository.download.DownloadStarter @@ -66,6 +67,7 @@ fun EnclosureBottomSheet( onDismissRequest: () -> Unit, sheetState: SheetState = rememberModalBottomSheetState(), dataList: List, + article: ArticleWithEnclosureBean, ) { val context = LocalContext.current val onDownload: (Any) -> Unit = remember { @@ -104,9 +106,17 @@ fun EnclosureBottomSheet( HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) } if (item is EnclosureBean) { - EnclosureItem(item, onDownload = onDownload) + EnclosureItem( + enclosure = item, + article = article, + onDownload = onDownload, + ) } else if (item is LinkEnclosureBean) { - LinkEnclosureItem(item, onDownload = onDownload) + LinkEnclosureItem( + enclosure = item, + article = article, + onDownload = onDownload, + ) } } } @@ -116,7 +126,8 @@ fun EnclosureBottomSheet( @Composable private fun EnclosureItem( - data: EnclosureBean, + enclosure: EnclosureBean, + article: ArticleWithEnclosureBean, onDownload: (EnclosureBean) -> Unit, ) { val context = LocalContext.current @@ -124,21 +135,21 @@ private fun EnclosureItem( Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { Text( - modifier = Modifier.clickable { data.url.copy(context) }, - text = data.url, + modifier = Modifier.clickable { enclosure.url.copy(context) }, + text = enclosure.url, style = MaterialTheme.typography.bodyMedium, maxLines = 4, ) Row(modifier = Modifier.padding(top = 6.dp)) { Text( - text = data.length.fileSize(context), + text = enclosure.length.fileSize(context), style = MaterialTheme.typography.labelMedium, maxLines = 1, ) - if (!data.type.isNullOrBlank()) { + if (!enclosure.type.isNullOrBlank()) { Text( modifier = Modifier.padding(start = 16.dp), - text = data.type, + text = enclosure.type, style = MaterialTheme.typography.labelMedium, maxLines = 1, ) @@ -146,11 +157,15 @@ private fun EnclosureItem( } } Spacer(modifier = Modifier.width(12.dp)) - if (data.isMedia) { + if (enclosure.isMedia) { AniVuIconButton( onClick = { try { - PlayActivity.play(context.activity, Uri.parse(data.url)) + PlayActivity.play( + context.activity, + uri = Uri.parse(enclosure.url), + title = article.article.title, + ) } catch (e: Exception) { e.printStackTrace() } @@ -160,7 +175,7 @@ private fun EnclosureItem( ) } AniVuIconButton( - onClick = { onDownload(data) }, + onClick = { onDownload(enclosure) }, imageVector = Icons.Outlined.Download, contentDescription = stringResource(id = R.string.download), ) @@ -169,29 +184,34 @@ private fun EnclosureItem( @Composable private fun LinkEnclosureItem( - data: LinkEnclosureBean, + enclosure: LinkEnclosureBean, + article: ArticleWithEnclosureBean, onDownload: (LinkEnclosureBean) -> Unit, ) { val context = LocalContext.current val isMagnetOrTorrent = rememberSaveable { - data.link.startsWith("magnet:") || - Regex("^(http|https)://.*\\.torrent$").matches(data.link) + enclosure.link.startsWith("magnet:") || + Regex("^(http|https)://.*\\.torrent$").matches(enclosure.link) } Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Text( modifier = Modifier .weight(1f) - .clickable { data.link.copy(context) }, - text = data.link, + .clickable { enclosure.link.copy(context) }, + text = enclosure.link, style = MaterialTheme.typography.bodyMedium, maxLines = 5, ) Spacer(modifier = Modifier.width(12.dp)) - if (data.isMedia) { + if (enclosure.isMedia) { AniVuIconButton( onClick = { try { - PlayActivity.play(context.activity, Uri.parse(data.link)) + PlayActivity.play( + context.activity, + uri = Uri.parse(enclosure.link), + title = article.article.title, + ) } catch (e: Exception) { e.printStackTrace() } @@ -202,7 +222,7 @@ private fun LinkEnclosureItem( } if (isMagnetOrTorrent) { AniVuIconButton( - onClick = { onDownload(data) }, + onClick = { onDownload(enclosure) }, imageVector = Icons.Outlined.Download, contentDescription = stringResource(id = R.string.download), ) 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 2ef35495..8fd7008f 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 @@ -97,10 +97,7 @@ fun MediaScreen(path: String, viewModel: MediaViewModel = hiltViewModel()) { if (result.pickFolder) { MediaLibLocationPreference.put(context, this, result.result) } else { - PlayActivity.play( - context.activity, - File(result.result).toUri(), - ) + PlayActivity.play(context.activity, uri = File(result.result).toUri()) } } diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/media/MediaViewModel.kt b/app/src/main/java/com/skyd/anivu/ui/screen/media/MediaViewModel.kt index 6045fe1d..f751a2bb 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/media/MediaViewModel.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/media/MediaViewModel.kt @@ -86,7 +86,7 @@ class MediaViewModel @Inject constructor( ).flatMapConcat { intent -> val path = if (intent is MediaIntent.Init) intent.path else (intent as MediaIntent.Refresh).path - mediaRepo.requestGroups(uriPath = path!!).map { + mediaRepo.requestGroups(path = path!!).map { MediaPartialStateChange.GroupsResult.Success(groups = it) }.startWith(MediaPartialStateChange.LoadingDialog.Show) .catchMap { MediaPartialStateChange.GroupsResult.Failed(it.message.toString()) } diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/media/list/EditMediaSheet.kt b/app/src/main/java/com/skyd/anivu/ui/screen/media/list/EditMediaSheet.kt index 4677862f..ee3ff553 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/media/list/EditMediaSheet.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/media/list/EditMediaSheet.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Badge import androidx.compose.material.icons.outlined.Check import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.DriveFileRenameOutline @@ -57,6 +58,7 @@ fun EditMediaSheet( currentGroup: MediaGroupBean, groups: List, onRename: (MediaBean, String) -> Unit, + onSetFileDisplayName: (MediaBean, String?) -> Unit, onDelete: (MediaBean) -> Unit, onGroupChange: (MediaGroupBean) -> Unit, openCreateGroupDialog: () -> Unit, @@ -65,6 +67,7 @@ fun EditMediaSheet( ModalBottomSheet(onDismissRequest = onDismissRequest) { var openRenameInputDialog by rememberSaveable { mutableStateOf(null) } + var openSetFileDisplayNameInputDialog by rememberSaveable { mutableStateOf(null) } Column( modifier = Modifier @@ -78,6 +81,9 @@ fun EditMediaSheet( OptionArea( onOpenWith = { mediaBean.file.toUri(context).openWith(context) }, onRenameClicked = { openRenameInputDialog = mediaBean.file.name }, + onSetFileDisplayNameClicked = { + openSetFileDisplayNameInputDialog = mediaBean.displayName.orEmpty() + }, onDelete = { onDelete(mediaBean) onDismissRequest() @@ -110,6 +116,23 @@ fun EditMediaSheet( onDismissRequest = { openRenameInputDialog = null } ) } + + if (openSetFileDisplayNameInputDialog != null) { + TextFieldDialog( + titleText = stringResource(R.string.nickname), + value = openSetFileDisplayNameInputDialog.orEmpty(), + onValueChange = { openSetFileDisplayNameInputDialog = it }, + singleLine = false, + enableConfirm = { true }, + onConfirm = { + onSetFileDisplayName(mediaBean, it.replace(Regex("[\\n\\r]"), "")) + openSetFileDisplayNameInputDialog = null + onDismissRequest() + }, + imeAction = ImeAction.Done, + onDismissRequest = { openSetFileDisplayNameInputDialog = null } + ) + } } } @@ -149,6 +172,7 @@ internal fun OptionArea( deleteWarningText: String = stringResource(id = R.string.media_screen_delete_file_warning), onOpenWith: (() -> Unit)? = null, onRenameClicked: (() -> Unit)? = null, + onSetFileDisplayNameClicked: (() -> Unit)? = null, onDelete: (() -> Unit)? = null, ) { var openDeleteWarningDialog by rememberSaveable { mutableStateOf(false) } @@ -179,6 +203,13 @@ internal fun OptionArea( onClick = onRenameClicked, ) } + if (onSetFileDisplayNameClicked != null) { + SheetChip( + icon = Icons.Outlined.Badge, + text = stringResource(id = R.string.nickname), + onClick = onSetFileDisplayNameClicked, + ) + } if (onDelete != null) { SheetChip( icon = Icons.Outlined.Delete, 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 8d7a8ab0..f6c3aaa3 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 @@ -145,7 +145,11 @@ fun Media1Item( Column { Text( modifier = Modifier.wrapContentHeight(), - text = data.displayName ?: fileNameWithoutExtension, + text = if (data.displayName.isNullOrBlank()) { + fileNameWithoutExtension + } else { + data.displayName + }, maxLines = 3, style = MaterialTheme.typography.titleSmall, ) diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaList.kt b/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaList.kt index 980ac5eb..8f7f5d90 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaList.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaList.kt @@ -119,6 +119,14 @@ internal fun MediaList( onRename = { oldMedia, newName -> dispatch(MediaListIntent.RenameFile(oldMedia.file, newName)) }, + onSetFileDisplayName = { media, displayName -> + dispatch( + MediaListIntent.SetFileDisplayName( + media = media, + displayName = displayName, + ) + ) + }, onRemove = { dispatch(MediaListIntent.DeleteFile(it.file)) }, contentPadding = innerPadding + contentPadding + fabPadding, ) @@ -156,6 +164,7 @@ private fun MediaList( onPlay: (MediaBean) -> Unit, onOpenDir: (MediaBean) -> Unit, onRename: (MediaBean, String) -> Unit, + onSetFileDisplayName: (MediaBean, String?) -> Unit, onRemove: (MediaBean) -> Unit, contentPadding: PaddingValues = PaddingValues(0.dp), ) { @@ -192,6 +201,7 @@ private fun MediaList( currentGroup = groupInfo!!.group, groups = groups, onRename = onRename, + onSetFileDisplayName = onSetFileDisplayName, onDelete = { onRemove(it) openEditMediaDialog = null 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 b551dc61..8f9080bb 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 @@ -1,6 +1,7 @@ package com.skyd.anivu.ui.screen.media.list import com.skyd.anivu.base.mvi.MviIntent +import com.skyd.anivu.model.bean.MediaBean import com.skyd.anivu.model.bean.MediaGroupBean import java.io.File @@ -11,4 +12,5 @@ sealed interface MediaListIntent : MviIntent { data class Refresh(val path: String?, val group: MediaGroupBean?) : MediaListIntent data class DeleteFile(val file: File) : MediaListIntent data class RenameFile(val file: File, val newName: String) : MediaListIntent + data class SetFileDisplayName(val media: MediaBean, val displayName: String?) : MediaListIntent } \ No newline at end of file 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 22f2b93d..11863ca7 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 @@ -107,4 +107,43 @@ internal sealed interface MediaListPartialStateChange { data class Success(val oldFile: File, val newFile: File) : RenameFileResult data class Failed(val msg: String) : RenameFileResult } + + sealed interface SetFileDisplayNameResult : MediaListPartialStateChange { + override fun reduce(oldState: MediaListState): MediaListState { + return when (this) { + is Success -> { + val listState = oldState.listState + oldState.copy( + listState = if (listState is ListState.Success) { + ListState.Success(listState.list.toMutableList().apply { + val index = indexOfFirst { it.file == media.file } + if (index in indices) { + val old = get(index) + removeAt(index) + add( + index, old.copy( + displayName = if (displayName.isNullOrBlank()) null + else displayName + ) + ) + } + }) + } else { + listState + }, + loadingDialog = false, + ) + } + + is Failed -> oldState.copy( + loadingDialog = false, + ) + } + } + + data class Success(val media: MediaBean, val displayName: String?) : + SetFileDisplayNameResult + + data class Failed(val msg: String) : SetFileDisplayNameResult + } } 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 ffceb2c8..b65ee622 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 @@ -66,8 +66,8 @@ class MediaListViewModel @Inject constructor( val group = if (intent is MediaListIntent.Init) intent.group else (intent as MediaListIntent.Refresh).group combine( - mediaRepo.requestFiles(uriPath = path!!, group), - mediaRepo.requestGroups(uriPath = path), + mediaRepo.requestFiles(path = path!!, group), + mediaRepo.requestGroups(path = path), ) { files, groups -> MediaListPartialStateChange.MediaListResult.Success( list = files, @@ -90,6 +90,14 @@ class MediaListViewModel @Inject constructor( }.startWith(MediaListPartialStateChange.LoadingDialog.Show) .catchMap { MediaListPartialStateChange.RenameFileResult.Failed(it.message.toString()) } }, + filterIsInstance().flatMapConcat { intent -> + mediaRepo.setFileDisplayName(intent.media, intent.displayName).map { + MediaListPartialStateChange.SetFileDisplayNameResult.Success( + media = intent.media, displayName = intent.displayName + ) + }.startWith(MediaListPartialStateChange.LoadingDialog.Show) + .catchMap { MediaListPartialStateChange.SetFileDisplayNameResult.Failed(it.message.toString()) } + }, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadPartialStateChange.kt b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadPartialStateChange.kt index cecc09b9..29958724 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadPartialStateChange.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadPartialStateChange.kt @@ -1,6 +1,6 @@ package com.skyd.anivu.ui.screen.read -import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean +import com.skyd.anivu.model.bean.article.ArticleWithFeed internal sealed interface ReadPartialStateChange { @@ -32,7 +32,7 @@ internal sealed interface ReadPartialStateChange { } } - data class Success(val article: ArticleWithEnclosureBean) : ArticleResult + data class Success(val article: ArticleWithFeed) : ArticleResult data class Failed(val msg: String) : ArticleResult data object Loading : ArticleResult } diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadScreen.kt b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadScreen.kt index 85d9fa60..89606e2c 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadScreen.kt @@ -30,6 +30,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AttachFile import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.Download +import androidx.compose.material.icons.outlined.Favorite +import androidx.compose.material.icons.outlined.FavoriteBorder import androidx.compose.material.icons.outlined.FormatSize import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.PlayCircleOutline @@ -90,7 +92,7 @@ import com.skyd.anivu.ext.openBrowser import com.skyd.anivu.ext.toDateTimeString import com.skyd.anivu.ext.toEncodedUrl 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.article.EnclosureBean import com.skyd.anivu.model.bean.article.RssMediaBean import com.skyd.anivu.model.preference.appearance.read.ReadTextSizePreference @@ -160,36 +162,46 @@ fun ReadScreen(articleId: String, viewModel: ReadViewModel = hiltViewModel()) { AniVuIconButton( enabled = uiState.articleState is ArticleState.Success, onClick = { - val articleState = viewModel.viewState.value.articleState + val articleState = uiState.articleState if (articleState is ArticleState.Success) { - val link = articleState.article.article.link + val article = articleState.article.articleWithEnclosure.article + val link = article.link + val title = article.title if (!link.isNullOrBlank()) { - link.openBrowser(context) + ShareUtil.shareText( + context = context, + text = if (title.isNullOrBlank()) link else "[$title] $link", + ) } } }, - imageVector = Icons.Outlined.Public, - contentDescription = stringResource(R.string.read_screen_open_browser), + imageVector = Icons.Outlined.Share, + contentDescription = stringResource(R.string.share), ) + val isFavorite = (uiState.articleState as? ArticleState.Success) + ?.article?.articleWithEnclosure?.article?.isFavorite == true AniVuIconButton( enabled = uiState.articleState is ArticleState.Success, onClick = { - val articleState = viewModel.viewState.value.articleState + val articleState = uiState.articleState if (articleState is ArticleState.Success) { - val link = articleState.article.article.link - val title = articleState.article.article.title - if (!link.isNullOrBlank()) { - ShareUtil.shareText( - context = context, - text = if (title.isNullOrBlank()) link else "[$title] $link", + dispatcher( + ReadIntent.Favorite( + articleId = articleId, + favorite = !isFavorite, ) - } + ) } }, - imageVector = Icons.Outlined.Share, - contentDescription = stringResource(R.string.share), + imageVector = if (isFavorite) Icons.Outlined.Favorite + else Icons.Outlined.FavoriteBorder, + contentDescription = stringResource( + if (isFavorite) R.string.article_screen_unfavorite + else R.string.article_screen_favorite + ), ) AniVuIconButton( + enabled = uiState.articleState is ArticleState.Success, onClick = { openMoreMenu = true }, imageVector = Icons.Outlined.MoreVert, contentDescription = stringResource(R.string.more), @@ -197,6 +209,15 @@ fun ReadScreen(articleId: String, viewModel: ReadViewModel = hiltViewModel()) { MoreMenu( expanded = openMoreMenu, onDismissRequest = { openMoreMenu = false }, + onOpenInBrowserClick = { + val articleState = uiState.articleState + if (articleState is ArticleState.Success) { + val link = articleState.article.articleWithEnclosure.article.link + if (!link.isNullOrBlank()) { + link.openBrowser(context) + } + } + }, onReadTextSizeClick = { openReadTextSizeSliderDialog = true }, ) } @@ -206,9 +227,11 @@ fun ReadScreen(articleId: String, viewModel: ReadViewModel = hiltViewModel()) { AniVuFloatingActionButton( onSizeWithSinglePaddingChanged = { _, height -> fabHeight = height }, onClick = { - val articleState = viewModel.viewState.value.articleState + val articleState = uiState.articleState if (articleState is ArticleState.Success) { - openEnclosureBottomSheet = getEnclosuresList(context, articleState.article) + openEnclosureBottomSheet = getEnclosuresList( + context, articleState.article.articleWithEnclosure, + ) } }, ) { @@ -244,19 +267,28 @@ fun ReadScreen(articleId: String, viewModel: ReadViewModel = hiltViewModel()) { ArticleState.Init, ArticleState.Loading -> Unit - is ArticleState.Success -> Content( - articleState = articleState, - shareImage = { dispatcher(ReadIntent.ShareImage(url = it)) }, - copyImage = { dispatcher(ReadIntent.CopyImage(url = it)) }, - downloadImage = { - dispatcher( - ReadIntent.DownloadImage( - url = it, - title = articleState.article.article.title, + is ArticleState.Success -> { + Content( + articleState = articleState, + shareImage = { dispatcher(ReadIntent.ShareImage(url = it)) }, + copyImage = { dispatcher(ReadIntent.CopyImage(url = it)) }, + downloadImage = { + dispatcher( + ReadIntent.DownloadImage( + url = it, + title = articleState.article.articleWithEnclosure.article.title, + ) ) + }, + ) + if (openEnclosureBottomSheet != null) { + EnclosureBottomSheet( + onDismissRequest = { openEnclosureBottomSheet = null }, + dataList = openEnclosureBottomSheet.orEmpty(), + article = articleState.article.articleWithEnclosure, ) - }, - ) + } + } } } @@ -276,13 +308,6 @@ fun ReadScreen(articleId: String, viewModel: ReadViewModel = hiltViewModel()) { WaitingDialog(visible = uiState.loadingDialog) - if (openEnclosureBottomSheet != null) { - EnclosureBottomSheet( - onDismissRequest = { openEnclosureBottomSheet = null }, - dataList = openEnclosureBottomSheet.orEmpty() - ) - } - if (openReadTextSizeSliderDialog) { ReadTextSizeSliderDialog( onDismissRequest = { openReadTextSizeSliderDialog = false }, @@ -315,7 +340,7 @@ private fun Content( shareImage: (url: String) -> Unit, ) { val context = LocalContext.current - val article = articleState.article + val article = articleState.article.articleWithEnclosure var openImageSheet by rememberSaveable { mutableStateOf(null) } SelectionContainer { @@ -364,8 +389,8 @@ private fun Content( } } } - MediaRow(articleWithEnclosureBean = article, onPlay = { url -> - PlayActivity.play(context.activity, Uri.parse(url)) + MediaRow(articleWithFeed = articleState.article, onPlay = { url -> + PlayActivity.play(context.activity, uri = Uri.parse(url), title = article.article.title) }) HtmlText( modifier = Modifier.padding(horizontal = 16.dp), @@ -395,9 +420,23 @@ private fun Content( private fun MoreMenu( expanded: Boolean, onDismissRequest: () -> Unit, + onOpenInBrowserClick: () -> Unit, onReadTextSizeClick: () -> Unit, ) { DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) { + DropdownMenuItem( + text = { Text(text = stringResource(R.string.read_screen_open_browser)) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Public, + contentDescription = null, + ) + }, + onClick = { + onDismissRequest() + onOpenInBrowserClick() + }, + ) DropdownMenuItem( text = { Text(text = stringResource(R.string.read_screen_text_size)) }, leadingIcon = { @@ -473,9 +512,10 @@ private fun RssMediaDuration(modifier: Modifier = Modifier, rssMedia: RssMediaBe } @Composable -private fun MediaRow(articleWithEnclosureBean: ArticleWithEnclosureBean, onPlay: (String) -> Unit) { - val enclosures = articleWithEnclosureBean.enclosures.filter { it.isMedia } - val cover = articleWithEnclosureBean.media?.image +private fun MediaRow(articleWithFeed: ArticleWithFeed, onPlay: (String) -> Unit) { + val articleWithEnclosure = articleWithFeed.articleWithEnclosure + val enclosures = articleWithEnclosure.enclosures.filter { it.isMedia } + val cover = articleWithEnclosure.media?.image ?: articleWithFeed.feed.icon if (enclosures.size > 1) { Spacer(modifier = Modifier.height(6.dp)) LazyRow( @@ -494,7 +534,7 @@ private fun MediaRow(articleWithEnclosureBean: ArticleWithEnclosureBean, onPlay: ) } } - articleWithEnclosureBean.media?.let { media -> + articleWithEnclosure.media?.let { media -> Spacer(modifier = Modifier.height(12.dp)) Row { RssMediaEpisode(rssMedia = media) @@ -520,7 +560,7 @@ private fun MediaRow(articleWithEnclosureBean: ArticleWithEnclosureBean, onPlay: enclosure = item, onClick = { onPlay(item.url) }, ) { - articleWithEnclosureBean.media?.let { media -> + articleWithEnclosure.media?.let { media -> RssMediaEpisode( modifier = Modifier .align(Alignment.TopEnd) diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadState.kt b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadState.kt index 93dfa857..50e96036 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadState.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadState.kt @@ -1,7 +1,7 @@ package com.skyd.anivu.ui.screen.read import com.skyd.anivu.base.mvi.MviViewState -import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean +import com.skyd.anivu.model.bean.article.ArticleWithFeed data class ReadState( val articleState: ArticleState, @@ -16,7 +16,7 @@ data class ReadState( } sealed interface ArticleState { - data class Success(val article: ArticleWithEnclosureBean) : ArticleState + data class Success(val article: ArticleWithFeed) : ArticleState data object Init : ArticleState data object Loading : ArticleState data class Failed(val msg: String) : ArticleState diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadViewModel.kt b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadViewModel.kt index ce2067ea..aef86084 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadViewModel.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadViewModel.kt @@ -77,7 +77,7 @@ class ReadViewModel @Inject constructor( return merge( filterIsInstance().flatMapConcat { intent -> articleRepo.readArticle(intent.articleId, read = true).flatMapConcat { - readRepo.requestArticleWithEnclosure(intent.articleId) + readRepo.requestArticleWithFeed(intent.articleId) }.map { if (it == null) { ReadPartialStateChange.ArticleResult.Failed( diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0e5576f7..9bf2fdcb 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -353,6 +353,7 @@ 文章通知 名称 重命名 + 昵称 下载任务 BT 任务 继续下载 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b82e20f4..ce44f4c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -360,6 +360,7 @@ Article notification Name Rename + Nickname Download Tasks BT Tasks Resume diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1bbfd5ac..17e78303 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ okhttp3 = "4.12.0" rome = "2.1.0" room = "2.6.1" kotlin = "2.0.21" -agp = "8.7.2" +agp = "8.7.3" [libraries] @@ -84,12 +84,12 @@ androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomato androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version = "1.3.3" } [plugins] -android-application = { id = "com.android.application", version = "8.7.2" } +android-application = { id = "com.android.application", version = "8.7.3" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } hilt = { id = "com.google.dagger.hilt.android", version = "2.51.1" } ksp = { id = "com.google.devtools.ksp", version = "2.0.21-1.0.25" } -android-library = { id = "com.android.library", version = "8.7.2" } +android-library = { id = "com.android.library", version = "8.7.3" } android-test = { id = "com.android.test", version.ref = "agp" }