From d7b3625a8e9b0566844d80f999caf5d7a5fac0a7 Mon Sep 17 00:00:00 2001 From: SkyD666 Date: Sat, 16 Dec 2023 22:30:42 +0800 Subject: [PATCH] [feature|build] Supports exporting stickers to Zip backup file when multiple selections on the search page or on the sticker list page; update dependencies version --- app/build.gradle | 10 +- .../rays/model/db/dao/sticker/StickerDao.kt | 13 +++ .../search/SearchResultSortPreference.kt | 10 +- .../ImportExportFilesRepository.kt | 19 +++- .../com/skyd/rays/ui/activity/MainActivity.kt | 4 +- .../ui/component/dialog/MultiChoiceDialog.kt | 104 ++++++++++++++++++ .../rays/ui/screen/detail/DetailScreen.kt | 6 +- .../skyd/rays/ui/screen/home/HomeScreen.kt | 26 ++++- .../rays/ui/screen/search/SearchResultList.kt | 10 ++ .../rays/ui/screen/search/SearchScreen.kt | 7 ++ .../data/importexport/ImportExportScreen.kt | 4 +- .../file/exportfiles/ExportFilesIntent.kt | 9 +- .../file/exportfiles/ExportFilesScreen.kt | 70 +++++++++++- .../file/exportfiles/ExportFilesViewModel.kt | 9 +- .../screen/stickerslist/StickersListScreen.kt | 18 +++ app/src/main/res/values-zh-rCN/strings.xml | 14 +-- app/src/main/res/values-zh-rTW/strings.xml | 14 +-- app/src/main/res/values/strings.xml | 14 +-- 18 files changed, 316 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/com/skyd/rays/ui/component/dialog/MultiChoiceDialog.kt diff --git a/app/build.gradle b/app/build.gradle index c25898c..032891a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,8 +21,8 @@ android { applicationId "com.skyd.rays" minSdk 24 targetSdk 34 - versionCode 55 - versionName "2.0-alpha12" + versionCode 56 + versionName "2.0-alpha13" flavorDimensions = ["versionName"] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -132,15 +132,15 @@ dependencies { implementation "androidx.compose.material:material:1.5.4" implementation "androidx.compose.material:material-icons-extended:1.5.4" implementation "androidx.compose.ui:ui-tooling-preview:$md3_version" - implementation "com.google.android.material:material:1.10.0" + implementation "com.google.android.material:material:1.11.0" implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.2" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" - implementation "androidx.activity:activity-compose:1.8.1" + implementation "androidx.activity:activity-compose:1.8.2" implementation "androidx.palette:palette-ktx:1.0.0" implementation "com.google.dagger:hilt-android:2.48.1" kapt "com.google.dagger:hilt-android-compiler:2.48.1" implementation "androidx.hilt:hilt-navigation-compose:1.1.0" - implementation "androidx.navigation:navigation-compose:2.7.5" + implementation "androidx.navigation:navigation-compose:2.7.6" implementation "androidx.security:security-crypto:1.1.0-alpha06" implementation "com.google.accompanist:accompanist-drawablepainter:$accompanist_version" implementation "io.coil-kt:coil-compose:2.5.0" diff --git a/app/src/main/java/com/skyd/rays/model/db/dao/sticker/StickerDao.kt b/app/src/main/java/com/skyd/rays/model/db/dao/sticker/StickerDao.kt index f904316..1cb40cc 100644 --- a/app/src/main/java/com/skyd/rays/model/db/dao/sticker/StickerDao.kt +++ b/app/src/main/java/com/skyd/rays/model/db/dao/sticker/StickerDao.kt @@ -43,6 +43,10 @@ interface StickerDao { @Query("SELECT * FROM $STICKER_TABLE_NAME") fun getAllStickerWithTagsList(): List + @Transaction + @Query("SELECT * FROM $STICKER_TABLE_NAME WHERE $UUID_COLUMN IN (:uuids)") + fun getAllStickerWithTagsList(uuids: Collection): List + @Transaction @Query("SELECT * FROM $STICKER_TABLE_NAME") fun getStickerList(): List @@ -182,6 +186,15 @@ interface StickerDao { .fromApplication(appContext, StickerDaoEntryPoint::class.java) var updatedCount = 0 stickerWithTagsList.forEach { + val currentTimeMillis = System.currentTimeMillis() + it.stickerWithTags.sticker.apply { + if (createTime == 0L) { + createTime = currentTimeMillis + } + if (modifyTime == 0L) { + modifyTime = currentTimeMillis + } + } val updated = proxy.handle( stickerDao = this, tagDao = hiltEntryPoint.tagDao, diff --git a/app/src/main/java/com/skyd/rays/model/preference/search/SearchResultSortPreference.kt b/app/src/main/java/com/skyd/rays/model/preference/search/SearchResultSortPreference.kt index 942baaf..f526c7e 100644 --- a/app/src/main/java/com/skyd/rays/model/preference/search/SearchResultSortPreference.kt +++ b/app/src/main/java/com/skyd/rays/model/preference/search/SearchResultSortPreference.kt @@ -36,12 +36,12 @@ object SearchResultSortPreference : BasePreference { override fun fromPreferences(preferences: Preferences): String = preferences[key] ?: default fun toDisplayName(sort: String): String = when (sort) { - "CreateTime" -> appContext.getString(R.string.search_result_sort_create_time) - "ModifyTime" -> appContext.getString(R.string.search_result_sort_modify_time) + "CreateTime" -> appContext.getString(R.string.sticker_create_time) + "ModifyTime" -> appContext.getString(R.string.sticker_modify_time) "TagCount" -> appContext.getString(R.string.search_result_sort_tag_count) "Title" -> appContext.getString(R.string.search_result_sort_title) - "ClickCount" -> appContext.getString(R.string.search_result_sort_click_count) - "ShareCount" -> appContext.getString(R.string.search_result_sort_share_count) - else -> appContext.getString(R.string.search_result_sort_create_time) + "ClickCount" -> appContext.getString(R.string.sticker_click_count) + "ShareCount" -> appContext.getString(R.string.sticker_share_count) + else -> appContext.getString(R.string.sticker_create_time) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/rays/model/respository/ImportExportFilesRepository.kt b/app/src/main/java/com/skyd/rays/model/respository/ImportExportFilesRepository.kt index 9d81afd..a04aa84 100644 --- a/app/src/main/java/com/skyd/rays/model/respository/ImportExportFilesRepository.kt +++ b/app/src/main/java/com/skyd/rays/model/respository/ImportExportFilesRepository.kt @@ -107,14 +107,29 @@ class ImportExportFilesRepository @Inject constructor( } } - suspend fun requestExport(dirUri: Uri): Flow { + suspend fun requestExport( + dirUri: Uri, + excludeClickCount: Boolean = false, + excludeShareCount: Boolean = false, + excludeCreateTime: Boolean = false, + excludeModifyTime: Boolean = false, + exportStickers: Collection? = null, + ): Flow { return flowOnIo { val startTime = System.currentTimeMillis() - val allStickerWithTagsList = stickerDao.getAllStickerWithTagsList() + val allStickerWithTagsList = if (exportStickers == null) { + stickerDao.getAllStickerWithTagsList() + } else { + stickerDao.getAllStickerWithTagsList(exportStickers) + } val totalCount = allStickerWithTagsList.size var currentCount = 0 EXPORT_FILES_DIR.deleteRecursively() allStickerWithTagsList.forEach { + if (excludeClickCount) it.sticker.clickCount = 0L + if (excludeShareCount) it.sticker.shareCount = 0L + if (excludeCreateTime) it.sticker.createTime = 0L + if (excludeModifyTime) it.sticker.modifyTime = 0L stickerWithTagsToJsonFile(it) stickerUuidToFile(it.sticker.uuid) .copyTo(File("$EXPORT_FILES_DIR/$BACKUP_STICKER_DIR", it.sticker.uuid)) diff --git a/app/src/main/java/com/skyd/rays/ui/activity/MainActivity.kt b/app/src/main/java/com/skyd/rays/ui/activity/MainActivity.kt index a0e229b..ba43483 100644 --- a/app/src/main/java/com/skyd/rays/ui/activity/MainActivity.kt +++ b/app/src/main/java/com/skyd/rays/ui/activity/MainActivity.kt @@ -240,7 +240,9 @@ class MainActivity : AppCompatActivity() { WebDavScreen() } composable(route = EXPORT_FILES_SCREEN_ROUTE) { - ExportFilesScreen() + ExportFilesScreen( + exportStickers = it.arguments?.getStringArrayList("exportStickers") + ) } composable(route = IMPORT_FILES_SCREEN_ROUTE) { ImportFilesScreen() diff --git a/app/src/main/java/com/skyd/rays/ui/component/dialog/MultiChoiceDialog.kt b/app/src/main/java/com/skyd/rays/ui/component/dialog/MultiChoiceDialog.kt new file mode 100644 index 0000000..dbc78e2 --- /dev/null +++ b/app/src/main/java/com/skyd/rays/ui/component/dialog/MultiChoiceDialog.kt @@ -0,0 +1,104 @@ +package com.skyd.rays.ui.component.dialog + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Checkbox +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import com.skyd.rays.R + +@Composable +fun MultiChoiceDialog( + modifier: Modifier = Modifier, + visible: Boolean, + onDismissRequest: () -> Unit = {}, + title: @Composable (() -> Unit)? = null, + options: List, + checkedIndexList: List, + onConfirm: (List) -> Unit, + confirmText: String = stringResource(id = R.string.dialog_ok), + dismissText: String? = stringResource(id = R.string.cancel), +) { + val selectedIndexList = remember(visible) { checkedIndexList.toMutableStateList() } + RaysDialog( + modifier = modifier, + visible = visible, + onDismissRequest = onDismissRequest, + title = title, + text = { + Column { + options.forEachIndexed { index, item -> + MultiChoiceItem( + checked = index in selectedIndexList, + text = item, + onClick = { + if (index in selectedIndexList) { + selectedIndexList.remove(index) + } else { + selectedIndexList.add(index) + } + }, + ) + } + } + }, + confirmButton = { + TextButton(onClick = { onConfirm(selectedIndexList) }) { Text(text = confirmText) } + }, + dismissButton = if (dismissText == null) null else { + { TextButton(onClick = onDismissRequest) { Text(text = dismissText) } } + }, + ) +} + +@Composable +private fun MultiChoiceItem( + checked: Boolean, + text: String, + onClick: () -> Unit +) { + val interactionSource = remember { MutableInteractionSource() } + Row( + Modifier + .fillMaxWidth() + .height(50.dp) + .clip(RoundedCornerShape(20)) + .selectable( + selected = checked, + onClick = onClick, + interactionSource = interactionSource, + indication = LocalIndication.current, + role = Role.Checkbox + ) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = checked, + interactionSource = interactionSource, + onCheckedChange = null // null recommended for accessibility with screen readers + ) + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(start = 16.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/rays/ui/screen/detail/DetailScreen.kt b/app/src/main/java/com/skyd/rays/ui/screen/detail/DetailScreen.kt index cd2340f..6190540 100644 --- a/app/src/main/java/com/skyd/rays/ui/screen/detail/DetailScreen.kt +++ b/app/src/main/java/com/skyd/rays/ui/screen/detail/DetailScreen.kt @@ -451,17 +451,17 @@ fun StickerDetailInfo(modifier: Modifier = Modifier, stickerWithTags: StickerWit ) DetailInfoItem( icon = Icons.Default.AdsClick, - title = stringResource(id = R.string.detail_screen_sticker_info_click_count), + title = stringResource(id = R.string.sticker_click_count), text = sticker.clickCount.toString(), ) DetailInfoItem( icon = Icons.Default.Share, - title = stringResource(id = R.string.detail_screen_sticker_info_share_count), + title = stringResource(id = R.string.sticker_share_count), text = sticker.shareCount.toString() ) DetailInfoItem( icon = Icons.Default.AddCircle, - title = stringResource(id = R.string.detail_screen_sticker_info_create_time), + title = stringResource(id = R.string.sticker_create_time), text = dateTime(sticker.createTime) ) DetailInfoItem( diff --git a/app/src/main/java/com/skyd/rays/ui/screen/home/HomeScreen.kt b/app/src/main/java/com/skyd/rays/ui/screen/home/HomeScreen.kt index 2fd0982..d689482 100644 --- a/app/src/main/java/com/skyd/rays/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/com/skyd/rays/ui/screen/home/HomeScreen.kt @@ -2,6 +2,7 @@ package com.skyd.rays.ui.screen.home import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -41,6 +42,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -63,6 +65,7 @@ import com.skyd.rays.ui.screen.add.openAddScreen import com.skyd.rays.ui.screen.detail.openDetailScreen import com.skyd.rays.ui.screen.search.SEARCH_SCREEN_ROUTE import com.skyd.rays.ui.screen.stickerslist.openStickersListScreen +import com.skyd.rays.util.sendStickerByUuid const val HOME_SCREEN_ROUTE = "homeScreen" @@ -70,6 +73,7 @@ const val HOME_SCREEN_ROUTE = "homeScreen" @Composable fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) { val navController = LocalNavController.current + val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } val uiState by viewModel.viewState.collectAsStateWithLifecycle() var fabHeight by remember { mutableStateOf(0.dp) } @@ -134,6 +138,12 @@ fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) { count = mostSharedStickersList.size, itemImage = { mostSharedStickersList[it].sticker.uuid }, itemTitle = { mostSharedStickersList[it].sticker.title }, + onItemLongClick = { + context.sendStickerByUuid( + uuid = mostSharedStickersList[it].sticker.uuid, + onSuccess = { mostSharedStickersList[it].sticker.shareCount++ } + ) + }, onItemClick = { openDetailScreen( navController = navController, @@ -149,6 +159,12 @@ fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) { count = recentCreatedStickersList.size, itemImage = { recentCreatedStickersList[it].sticker.uuid }, itemTitle = { recentCreatedStickersList[it].sticker.title }, + onItemLongClick = { + context.sendStickerByUuid( + uuid = recentCreatedStickersList[it].sticker.uuid, + onSuccess = { recentCreatedStickersList[it].sticker.shareCount++ } + ) + }, onItemClick = { openDetailScreen( navController = navController, @@ -203,6 +219,7 @@ private fun DisplayStickersRow( count: Int, itemImage: (Int) -> String, itemTitle: (Int) -> String, + onItemLongClick: (Int) -> Unit, onItemClick: (Int) -> Unit, ) { Column { @@ -227,11 +244,16 @@ private fun DisplayStickersRow( ) { items(count) { index -> Column(modifier = Modifier.width(IntrinsicSize.Min)) { - ElevatedCard(onClick = { onItemClick(index) }) { + ElevatedCard { RaysImage( modifier = Modifier .height(150.dp) - .aspectRatio(1f), + .aspectRatio(1f) + .combinedClickable( + onLongClick = { onItemLongClick(index) }, + onClick = { onItemClick(index) } + + ), uuid = itemImage(index), contentScale = ContentScale.Crop, ) diff --git a/app/src/main/java/com/skyd/rays/ui/screen/search/SearchResultList.kt b/app/src/main/java/com/skyd/rays/ui/screen/search/SearchResultList.kt index e1ef3c3..88bcf51 100644 --- a/app/src/main/java/com/skyd/rays/ui/screen/search/SearchResultList.kt +++ b/app/src/main/java/com/skyd/rays/ui/screen/search/SearchResultList.kt @@ -32,6 +32,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.ArrowDropUp import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.FolderZip import androidx.compose.material.icons.filled.Save import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.outlined.CheckCircle @@ -224,6 +225,7 @@ internal fun MultiSelectActionBar( selectedStickers: List, onDeleteClick: () -> Unit, onExportClick: () -> Unit, + onExportAsZipClick: () -> Unit, ) { val context = LocalContext.current val windowSizeClass = LocalWindowSizeClass.current @@ -256,6 +258,14 @@ internal fun MultiSelectActionBar( contentDescription = stringResource(id = R.string.home_screen_export) ) }, + @Composable { + RaysIconButton( + onClick = onExportAsZipClick, + enabled = selectedStickers.isNotEmpty(), + imageVector = Icons.Default.FolderZip, + contentDescription = stringResource(id = R.string.home_screen_export_to_backup_zip) + ) + }, @Composable { RaysIconButton( onClick = onDeleteClick, diff --git a/app/src/main/java/com/skyd/rays/ui/screen/search/SearchScreen.kt b/app/src/main/java/com/skyd/rays/ui/screen/search/SearchScreen.kt index c68f46c..0ac2f9f 100644 --- a/app/src/main/java/com/skyd/rays/ui/screen/search/SearchScreen.kt +++ b/app/src/main/java/com/skyd/rays/ui/screen/search/SearchScreen.kt @@ -94,6 +94,7 @@ import com.skyd.rays.ui.local.LocalQuery import com.skyd.rays.ui.local.LocalShowPopularTags import com.skyd.rays.ui.local.LocalWindowSizeClass import com.skyd.rays.ui.screen.detail.openDetailScreen +import com.skyd.rays.ui.screen.settings.data.importexport.file.exportfiles.openExportFilesScreen import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -245,6 +246,12 @@ fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) { searchResultUiState.stickerWithTagsList.toSet() }, onExportClick = { openMultiStickersExportPathDialog = true }, + onExportAsZipClick = { + openExportFilesScreen( + navController = navController, + exportStickers = selectedStickers.map { it.sticker.uuid }, + ) + }, ) ExportDialog( visible = openMultiStickersExportPathDialog, diff --git a/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/ImportExportScreen.kt b/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/ImportExportScreen.kt index 90ee513..6811453 100644 --- a/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/ImportExportScreen.kt +++ b/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/ImportExportScreen.kt @@ -21,7 +21,7 @@ import com.skyd.rays.ui.component.RaysTopBar import com.skyd.rays.ui.component.RaysTopBarStyle import com.skyd.rays.ui.local.LocalNavController import com.skyd.rays.ui.screen.settings.data.importexport.cloud.webdav.WEBDAV_SCREEN_ROUTE -import com.skyd.rays.ui.screen.settings.data.importexport.file.exportfiles.EXPORT_FILES_SCREEN_ROUTE +import com.skyd.rays.ui.screen.settings.data.importexport.file.exportfiles.openExportFilesScreen import com.skyd.rays.ui.screen.settings.data.importexport.file.importfiles.IMPORT_FILES_SCREEN_ROUTE const val IMPORT_EXPORT_SCREEN_ROUTE = "importExportScreen" @@ -77,7 +77,7 @@ fun ImportExportScreen() { icon = rememberVectorPainter(image = Icons.Default.Upload), text = stringResource(id = R.string.export_files_screen_name), descriptionText = stringResource(id = R.string.export_files_screen_description), - onClick = { navController.navigate(EXPORT_FILES_SCREEN_ROUTE) } + onClick = { openExportFilesScreen(navController = navController) } ) } } diff --git a/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/file/exportfiles/ExportFilesIntent.kt b/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/file/exportfiles/ExportFilesIntent.kt index f030658..c92560e 100644 --- a/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/file/exportfiles/ExportFilesIntent.kt +++ b/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/file/exportfiles/ExportFilesIntent.kt @@ -5,5 +5,12 @@ import com.skyd.rays.base.mvi.MviIntent sealed interface ExportFilesIntent : MviIntent { data object Init : ExportFilesIntent - data class Export(val dirUri: Uri) : ExportFilesIntent + data class Export( + val dirUri: Uri, + val excludeClickCount: Boolean = false, + val excludeShareCount: Boolean = false, + val excludeCreateTime: Boolean = false, + val excludeModifyTime: Boolean = false, + val exportStickers: Collection? = null, + ) : ExportFilesIntent } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/file/exportfiles/ExportFilesScreen.kt b/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/file/exportfiles/ExportFilesScreen.kt index 035f403..10ee1cf 100644 --- a/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/file/exportfiles/ExportFilesScreen.kt +++ b/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/file/exportfiles/ExportFilesScreen.kt @@ -2,6 +2,7 @@ package com.skyd.rays.ui.screen.settings.data.importexport.file.exportfiles import android.content.Intent import android.net.Uri +import android.os.Bundle import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.PaddingValues @@ -11,6 +12,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Folder +import androidx.compose.material.icons.filled.LayersClear import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -21,6 +23,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -33,8 +36,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController import com.skyd.rays.R import com.skyd.rays.base.mvi.getDispatcher +import com.skyd.rays.ext.navigate import com.skyd.rays.ext.plus import com.skyd.rays.ext.showSnackbarWithLaunchedEffect import com.skyd.rays.model.bean.ImportExportResultInfo @@ -43,19 +48,38 @@ import com.skyd.rays.ui.component.BaseSettingsItem import com.skyd.rays.ui.component.RaysExtendedFloatingActionButton import com.skyd.rays.ui.component.RaysTopBar import com.skyd.rays.ui.component.RaysTopBarStyle +import com.skyd.rays.ui.component.dialog.MultiChoiceDialog import com.skyd.rays.ui.component.dialog.RaysDialog import com.skyd.rays.ui.component.dialog.WaitingDialog const val EXPORT_FILES_SCREEN_ROUTE = "exportFilesScreen" +fun openExportFilesScreen( + navController: NavHostController, + exportStickers: List? = null, +) { + navController.navigate( + EXPORT_FILES_SCREEN_ROUTE, + Bundle().apply { + if (!exportStickers.isNullOrEmpty()) { + putStringArrayList("exportStickers", ArrayList(exportStickers)) + } + } + ) +} + @Composable -fun ExportFilesScreen(viewModel: ExportFilesViewModel = hiltViewModel()) { +fun ExportFilesScreen( + exportStickers: List? = null, + viewModel: ExportFilesViewModel = hiltViewModel() +) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } val uiState by viewModel.viewState.collectAsStateWithLifecycle() val uiEvent by viewModel.singleEvent.collectAsStateWithLifecycle(initialValue = null) + var openMultiChoiceDialog by rememberSaveable { mutableStateOf(false) } var openExportDialog by rememberSaveable { mutableStateOf(null) } var waitingDialogData by rememberSaveable { mutableStateOf(null) } @@ -76,6 +100,14 @@ fun ExportFilesScreen(viewModel: ExportFilesViewModel = hiltViewModel()) { val lazyListState = rememberLazyListState() var fabHeight by remember { mutableStateOf(0.dp) } + val excludeCheckedList = remember { mutableStateListOf() } + val excludeOptions = listOf( + stringResource(R.string.sticker_click_count), + stringResource(R.string.sticker_share_count), + stringResource(R.string.sticker_create_time), + stringResource(R.string.sticker_modify_time), + ) + Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, topBar = { @@ -89,7 +121,18 @@ fun ExportFilesScreen(viewModel: ExportFilesViewModel = hiltViewModel()) { RaysExtendedFloatingActionButton( text = { Text(text = stringResource(R.string.export_files_screen_export)) }, icon = { Icon(imageVector = Icons.Default.Done, contentDescription = null) }, - onClick = { dispatch(ExportFilesIntent.Export(exportDir)) }, + onClick = { + dispatch( + ExportFilesIntent.Export( + dirUri = exportDir, + excludeClickCount = 0 in excludeCheckedList, + excludeShareCount = 1 in excludeCheckedList, + excludeCreateTime = 2 in excludeCheckedList, + excludeModifyTime = 3 in excludeCheckedList, + exportStickers = exportStickers, + ) + ) + }, onSizeWithSinglePaddingChanged = { _, height -> fabHeight = height }, contentDescription = stringResource(R.string.export_files_screen_export) ) @@ -110,9 +153,32 @@ fun ExportFilesScreen(viewModel: ExportFilesViewModel = hiltViewModel()) { onClick = { pickExportDirLauncher.launch(exportDir) } ) } + item { + BaseSettingsItem( + icon = rememberVectorPainter(image = Icons.Default.LayersClear), + text = stringResource(R.string.export_files_screen_exclude_field), + descriptionText = excludeOptions.filterIndexed { index, _ -> + index in excludeCheckedList + }.joinToString(separator = ", ").ifBlank { null }, + onClick = { openMultiChoiceDialog = true }, + ) + } } } + MultiChoiceDialog( + visible = openMultiChoiceDialog, + onDismissRequest = { openMultiChoiceDialog = false }, + title = { Text(text = stringResource(R.string.export_files_screen_exclude_field)) }, + options = excludeOptions, + checkedIndexList = excludeCheckedList, + onConfirm = { + openMultiChoiceDialog = false + excludeCheckedList.clear() + excludeCheckedList.addAll(it) + }, + ) + WaitingDialog( visible = uiState.loadingDialog, currentValue = waitingDialogData?.current, diff --git a/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/file/exportfiles/ExportFilesViewModel.kt b/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/file/exportfiles/ExportFilesViewModel.kt index 77c1cbe..8835387 100644 --- a/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/file/exportfiles/ExportFilesViewModel.kt +++ b/app/src/main/java/com/skyd/rays/ui/screen/settings/data/importexport/file/exportfiles/ExportFilesViewModel.kt @@ -71,7 +71,14 @@ class ExportFilesViewModel @Inject constructor( filterIsInstance().map { ExportFilesPartialStateChange.Init }, filterIsInstance().flatMapConcat { intent -> - importExportFilesRepo.requestExport(intent.dirUri).map { + importExportFilesRepo.requestExport( + dirUri = intent.dirUri, + excludeClickCount = intent.excludeClickCount, + excludeShareCount = intent.excludeShareCount, + excludeCreateTime = intent.excludeCreateTime, + excludeModifyTime = intent.excludeModifyTime, + exportStickers = intent.exportStickers, + ).map { when (it) { is ImportExportResultInfo -> { ExportFilesPartialStateChange.ExportFilesProgress.Finish(it) diff --git a/app/src/main/java/com/skyd/rays/ui/screen/stickerslist/StickersListScreen.kt b/app/src/main/java/com/skyd/rays/ui/screen/stickerslist/StickersListScreen.kt index 9ec1369..a3230a7 100644 --- a/app/src/main/java/com/skyd/rays/ui/screen/stickerslist/StickersListScreen.kt +++ b/app/src/main/java/com/skyd/rays/ui/screen/stickerslist/StickersListScreen.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FolderZip import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -23,11 +25,13 @@ import com.skyd.rays.base.mvi.getDispatcher import com.skyd.rays.ext.isCompact import com.skyd.rays.ext.navigate import com.skyd.rays.ext.plus +import com.skyd.rays.ui.component.RaysIconButton import com.skyd.rays.ui.component.RaysTopBar import com.skyd.rays.ui.local.LocalNavController import com.skyd.rays.ui.local.LocalWindowSizeClass import com.skyd.rays.ui.screen.detail.openDetailScreen import com.skyd.rays.ui.screen.search.SearchResultItem +import com.skyd.rays.ui.screen.settings.data.importexport.file.exportfiles.openExportFilesScreen const val STICKERS_LIST_SCREEN_ROUTE = "stickersListScreen" @@ -57,6 +61,20 @@ fun StickersListScreen(query: String, viewModel: StickersListViewModel = hiltVie RaysTopBar( scrollBehavior = scrollBehavior, title = { Text(text = stringResource(R.string.stickers_list_screen_name)) }, + actions = { + RaysIconButton( + enabled = uiState.listState is ListState.Success, + onClick = { + openExportFilesScreen( + navController = navController, + exportStickers = (uiState.listState as? ListState.Success) + ?.stickerWithTagsList?.map { it.sticker.uuid }, + ) + }, + imageVector = Icons.Default.FolderZip, + contentDescription = stringResource(id = R.string.stickers_list_screen_export_current_stickers), + ) + } ) } ) { paddingValues -> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index ff475f6..083a014 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -77,6 +77,7 @@ 导出为文件 将表情包、标签等信息导出为文件 选择导出文件夹 + 排除 导出 导出中… 压缩中:%s @@ -185,8 +186,8 @@ 下载 忽略本次更新 检查更新失败:%s - 创建时间 - 修改时间 + 创建时间 + 修改时间 标签数量 标题 排序 @@ -205,9 +206,6 @@ 表情包信息 UUID 表情包 MD5 - 点击数 - 分享数 - 创建时间 上次修改时间 选择导出文件夹 请选择导出文件夹 @@ -223,8 +221,8 @@ 填充高度 合适 拉伸填充 - 点击数 - 分享数 + 点击数 + 分享数 分享 URI 字符串分享 分享时复制表情包 URI 字符串 @@ -297,6 +295,8 @@ 随机标签 最近创建 最多分享 + 导出为 Zip 备份文件 表情包列表 + 导出为 Zip 备份文件 删除 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 10d3e4f..aee2af1 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -63,7 +63,6 @@ 推送 無效格式,魔數不等於 0x0D000721 移動到遠端回收站 - 創建時間 正在壓縮:%s 在搜尋欄下方顯示分享次數最多的表情包對應的標籤 標籤數量 @@ -101,7 +100,6 @@ 導入與匯出 深色模式 正則表達式 - 點擊次數 只有置信度高于此門檻值的標籤才會被建議,建議設定在 0.5 左右。 伺服器 加入 Discord 伺服器 @@ -184,7 +182,6 @@ 共匯出 %1$d 張表情包,耗時 %2$.2f 秒 - 分享次數 在將本地數據推送到遠端時,如果存在衝突,本地表情包將覆蓋遠端內容。如果本地不存在但在遠端存在的表情包,將被移動到遠端回收站。 同步 關閉 @@ -198,7 +195,7 @@ 加入 Telegram 群組 管理外部應用程式的 API 權限 停用螢幕截圖 - 分享次數 + 分享次數 選擇外部模型 選擇匯出文件夾 空空如也呢~ @@ -222,10 +219,11 @@ 替換原有 小工具 選擇匯出目錄 + 排除 排序 請輸入 WebDAV 伺服器 URL 一般 - 創建時間 + 創建時間 隱私 只有置信度高於此閾值的文字才會被推薦,建議設定在 0.4 左右。 文本辨識 @@ -268,12 +266,12 @@ 沒有軟體包可用:%s 建議的標籤 熱門值:%.2f - 修改時間 + 修改時間 內部 標籤表 「衝突」指的是您當前正在導入的表情包的 UUID 與現有表情包的 UUID 相同,與這兩個表情包的內容是否相同無關。 沒有調色板 - 點擊次數 + 點擊次數 搜尋頁風格 放棄目前的編輯 處理清單 @@ -297,6 +295,8 @@ 隨機標籤 最近創建 最多分享 + 匯出為 Zip 備份文件 表情包列表 + 匯出為 Zip 備份文件 刪除 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3624e6..6359409 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -79,6 +79,7 @@ Export to files Export stickers, tags, and other information to the local files Select export directory + Exclude Export Exporting… Zipping: %s @@ -194,8 +195,8 @@ Download Ignore this update Check for update failed: %s - Create time - Modify time + Create time + Modify time Number of tags Title Sort @@ -214,9 +215,6 @@ Sticker info UUID Sticker MD5 - Click count - Share count - Create time Last modified time Select export folder Please select an export folder @@ -233,8 +231,8 @@ Fill height Fit Fill bounds - Click count - Share count + Click count + Share count Share URI string share Copy sticker URI string when sharing @@ -309,6 +307,8 @@ Random tags Recently created Most shared + Export as backup zip Stickers list + Export as backup zip Delete \ No newline at end of file