diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 39608345..9aa50af1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,7 +22,7 @@ android { minSdk = 24 targetSdk = 34 versionCode = 18 - versionName = "1.1-beta56" + versionName = "1.1-beta57" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -159,17 +159,17 @@ dependencies { implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.7.0") - implementation("androidx.activity:activity-ktx:1.9.0") + implementation("androidx.activity:activity-ktx:1.9.1") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha13") implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") implementation("androidx.navigation:navigation-ui-ktx:2.7.7") implementation("androidx.navigation:navigation-compose:2.7.7") - implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.3") - implementation("androidx.compose.ui:ui:1.7.0-beta05") - implementation("androidx.compose.material:material:1.7.0-beta05") - implementation("androidx.compose.material3:material3:1.3.0-beta04") - implementation("androidx.compose.material3:material3-window-size-class:1.3.0-beta04") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.4") + implementation("androidx.compose.ui:ui:1.7.0-beta06") + implementation("androidx.compose.material:material:1.7.0-beta06") + implementation("androidx.compose.material3:material3:1.3.0-beta05") + implementation("androidx.compose.material3:material3-window-size-class:1.3.0-beta05") implementation("androidx.compose.material3.adaptive:adaptive:1.0.0-beta04") implementation("androidx.compose.material3.adaptive:adaptive-layout:1.0.0-beta04") implementation("androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-beta04") @@ -183,8 +183,8 @@ dependencies { implementation("androidx.datastore:datastore-preferences:1.1.1") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.core:core-splashscreen:1.0.1") - implementation("androidx.paging:paging-runtime-ktx:3.3.0") - implementation("androidx.paging:paging-compose:3.3.0") + implementation("androidx.paging:paging-runtime-ktx:3.3.1") + implementation("androidx.paging:paging-compose:3.3.1") implementation("androidx.preference:preference-ktx:1.2.1") implementation("com.google.android.material:material:1.12.0") diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/filepicker/FilePickerFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/filepicker/FilePickerFragment.kt index a525a447..44939318 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/filepicker/FilePickerFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/filepicker/FilePickerFragment.kt @@ -1,6 +1,7 @@ package com.skyd.anivu.ui.fragment.filepicker import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -66,6 +67,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.onEach +import kotlinx.parcelize.Parcelize import java.io.File @@ -79,6 +81,7 @@ class FilePickerFragment : BaseComposeFragment() { val path = arguments?.getString(PATH_KEY) val pickFolder = arguments?.getBoolean(PICK_FOLDER_KEY) val extensionName = arguments?.getString(EXTENSION_NAME_KEY) + val id = arguments?.getString(PICK_FOLDER_ID_KEY) if (path == null || pickFolder == null || extensionName == null) { findMainNavController().popBackStackWithLifecycle() } else { @@ -86,6 +89,7 @@ class FilePickerFragment : BaseComposeFragment() { path = path, pickFolder = pickFolder, extensionName = extensionName, + id = id, ) } } @@ -95,16 +99,21 @@ const val PATH_KEY = "path" const val PICK_FOLDER_KEY = "pickFolder" const val EXTENSION_NAME_KEY = "extensionName" const val FILE_PICKER_NEW_PATH_KEY = "newPath" +const val PICK_FOLDER_ID_KEY = "id" @Composable -fun ListenToFilePicker(onNewPath: CoroutineScope.(String) -> Unit) { +fun ListenToFilePicker(onNewPath: CoroutineScope.(FilePickerResult) -> Unit) { val navController = LocalNavController.current LaunchedEffect(Unit) { - navController.currentBackStackEntry?.savedStateHandle - ?.getStateFlow(FILE_PICKER_NEW_PATH_KEY, null) - ?.filterNotNull() - ?.onEach { this.onNewPath(it) } - ?.collect() + navController.currentBackStackEntry?.savedStateHandle?.apply { + getStateFlow(FILE_PICKER_NEW_PATH_KEY, null) + .filterNotNull() + .onEach { + onNewPath(it) + remove(FILE_PICKER_NEW_PATH_KEY) + } + .collect() + } } } @@ -113,6 +122,7 @@ fun navigateToFilePicker( path: String, pickFolder: Boolean = true, extensionName: String = "", + id: String? = null, ) { navController.navigate( R.id.action_to_file_picker_fragment, @@ -125,16 +135,27 @@ fun navigateToFilePicker( } else path ) putString(EXTENSION_NAME_KEY, extensionName) + putString(PICK_FOLDER_ID_KEY, id) } ) } +@Parcelize +data class FilePickerResult( + val id: String?, + val path: String, + val pickFolder: Boolean, + val extensionName: String?, + val result: String, +) : Parcelable + @Composable fun FilePickerScreen( path: String, pickFolder: Boolean = false, extensionName: String? = null, + id: String?, viewModel: FilePickerViewModel = hiltViewModel(), ) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) @@ -168,7 +189,14 @@ fun FilePickerScreen( AniVuTopBar( style = AniVuTopBarStyle.Small, scrollBehavior = scrollBehavior, - title = { Text(text = stringResource(R.string.file_picker_screen_name)) }, + title = { + Text( + text = stringResource( + if (pickFolder) R.string.file_picker_screen_open_folder + else R.string.file_picker_screen_open_file + ) + ) + }, navigationIcon = { AniVuIconButton( onClick = { navController.popBackStackWithLifecycle() }, @@ -211,7 +239,17 @@ fun FilePickerScreen( if (!pickFolder) { navController.previousBackStackEntry ?.savedStateHandle - ?.set(FILE_PICKER_NEW_PATH_KEY, file.absolutePath) + ?.set( + FILE_PICKER_NEW_PATH_KEY, + FilePickerResult( + id = id, + path = path, + pickFolder = false, + extensionName = extensionName, + result = file.absolutePath, + ) + ) + navController.popBackStackWithLifecycle() } } }, @@ -243,7 +281,16 @@ fun FilePickerScreen( onClick = { navController.previousBackStackEntry ?.savedStateHandle - ?.set(FILE_PICKER_NEW_PATH_KEY, uiState.path) + ?.set( + FILE_PICKER_NEW_PATH_KEY, + FilePickerResult( + id = id, + path = path, + pickFolder = true, + extensionName = extensionName, + result = uiState.path, + ) + ) navController.popBackStackWithLifecycle() }, ) { diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/media/MediaScreen.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/media/MediaScreen.kt index 2c6a54df..2070483b 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/media/MediaScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/media/MediaScreen.kt @@ -1,6 +1,7 @@ package com.skyd.anivu.ui.fragment.media import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets @@ -16,13 +17,16 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.FileOpen import androidx.compose.material.icons.outlined.MyLocation import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Workspaces import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.Scaffold +import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Tab @@ -37,19 +41,25 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.skyd.anivu.R import com.skyd.anivu.base.mvi.getDispatcher +import com.skyd.anivu.ext.activity import com.skyd.anivu.ext.isCompact import com.skyd.anivu.ext.showSnackbarWithLaunchedEffect import com.skyd.anivu.model.bean.MediaGroupBean import com.skyd.anivu.model.preference.data.medialib.MediaLibLocationPreference +import com.skyd.anivu.ui.activity.PlayActivity import com.skyd.anivu.ui.component.AniVuFloatingActionButton import com.skyd.anivu.ui.component.AniVuIconButton import com.skyd.anivu.ui.component.AniVuTopBar @@ -63,6 +73,7 @@ import com.skyd.anivu.ui.fragment.media.list.MediaList import com.skyd.anivu.ui.local.LocalNavController import com.skyd.anivu.ui.local.LocalWindowSizeClass import kotlinx.coroutines.launch +import java.io.File import kotlin.math.min const val MEDIA_SCREEN_ROUTE = "mediaScreen" @@ -86,8 +97,15 @@ fun MediaScreen(path: String, viewModel: MediaViewModel = hiltViewModel()) { val pagerState = rememberPagerState(pageCount = { uiState.groups.size }) var openEditGroupDialog by rememberSaveable { mutableStateOf(value = null) } - ListenToFilePicker { newPath -> - MediaLibLocationPreference.put(context, this, newPath) + ListenToFilePicker { result -> + if (result.pickFolder) { + MediaLibLocationPreference.put(context, this, result.result) + } else { + PlayActivity.play( + context.activity, + File(result.result).toUri(), + ) + } } Scaffold( @@ -129,14 +147,40 @@ fun MediaScreen(path: String, viewModel: MediaViewModel = hiltViewModel()) { ) }, floatingActionButton = { - AniVuFloatingActionButton( - onClick = { openEditGroupDialog = uiState.groups[pagerState.currentPage].first }, - onSizeWithSinglePaddingChanged = { width, height -> - fabWidth = width - fabHeight = height + val density = LocalDensity.current + Column( + modifier = Modifier.onSizeChanged { + with(density) { + fabWidth = it.width.toDp() + 16.dp + fabHeight = it.height.toDp() + 16.dp + } }, + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.End ) { - Icon(imageVector = Icons.Outlined.Edit, contentDescription = null) + SmallFloatingActionButton( + onClick = { + navigateToFilePicker( + navController = navController, + path = path, + pickFolder = false, + ) + }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.secondary, + ) { + Icon( + imageVector = Icons.Outlined.FileOpen, + contentDescription = stringResource(id = R.string.open_file), + ) + } + AniVuFloatingActionButton( + onClick = { + openEditGroupDialog = uiState.groups[pagerState.currentPage].first + }, + ) { + Icon(imageVector = Icons.Outlined.Edit, contentDescription = null) + } } }, contentWindowInsets = WindowInsets.safeDrawing.run { diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/settings/data/DataFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/data/DataFragment.kt index 79f4faf9..b785a06b 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/settings/data/DataFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/data/DataFragment.kt @@ -77,8 +77,8 @@ fun DataScreen(viewModel: DataViewModel = hiltViewModel()) { val uiEvent by viewModel.singleEvent.collectAsStateWithLifecycle(initialValue = null) val dispatch = viewModel.getDispatcher(startWith = DataIntent.Init) - ListenToFilePicker { newPath -> - MediaLibLocationPreference.put(context, this, newPath) + ListenToFilePicker { result -> + MediaLibLocationPreference.put(context, this, result.result) } Scaffold( diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 69a00460..0e2fe6a6 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -204,7 +204,7 @@ + android:label="@string/file_picker_screen_open_file" /> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0ba8ccc5..854bc259 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -289,7 +289,8 @@ 没有存储权限无法运行应用程序,请授予权限。 AniVu 需要存储访问 授予权限 - 文件选择器 + 打开文件 + 打开文件夹 选择此文件夹 内部存储 删除此文件? @@ -305,6 +306,7 @@ 自动画中画 如果按下主页或最近任务按钮,则切换到画中画模式 画中画 + 打开文件 已读 %d 项 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6b4506c7..7ea764f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -296,7 +296,8 @@ Application can\'t be launched without storage permission, please grant the permission AniVu requires storage access Request permission - File picker + Open file + Open a folder Pick this folder Internal storage Delete this file? @@ -312,6 +313,7 @@ Auto PIP Switch to PiP mode if the Home or Recents button is pressed Picture in picture + Open file Read %d item Read %d items