Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5945dfacc | ||
|
|
4623faa87f | ||
|
|
18cc15ed07 | ||
|
|
8d36b340cf | ||
|
|
37dfde47a9 | ||
|
|
a5bb20b1d2 | ||
|
|
596ac96e77 | ||
|
|
4df4727535 | ||
|
|
3ce1b4a40f | ||
|
|
90dad3a9de | ||
|
|
e47912edf6 | ||
|
|
7156582046 | ||
|
|
71ad849166 | ||
|
|
761db7a69d | ||
|
|
428749d8a8 | ||
|
|
460c50bfd9 | ||
|
|
5832e2db4a | ||
|
|
76ea8ba9a0 | ||
|
|
c2ddfdf89d |
No files matched your search
@@ -9,8 +9,13 @@ import androidx.datastore.preferences.core.floatPreferencesKey
|
|||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -41,17 +46,26 @@ class UserPreferencesRepository @Inject constructor(
|
|||||||
val SCREEN_TRANSITIONS_ENABLED = booleanPreferencesKey("screen_transitions_enabled")
|
val SCREEN_TRANSITIONS_ENABLED = booleanPreferencesKey("screen_transitions_enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
val userPreferencesFlow: Flow<UserPreferences> = context.dataStore.data.map { prefs ->
|
// Hot, process-wide cache: the DataStore file is read once, and every screen
|
||||||
UserPreferences(
|
// created later receives the current value synchronously on subscription,
|
||||||
rootFolderUri = prefs[Keys.ROOT_FOLDER_URI],
|
// instead of re-reading disk per screen and flashing default positions.
|
||||||
cameraFabXFraction = prefs[Keys.CAMERA_FAB_X_FRACTION]?.coerceIn(0f, 1f) ?: 1f,
|
val userPreferencesFlow: StateFlow<UserPreferences> = context.dataStore.data
|
||||||
cameraFabYFraction = prefs[Keys.CAMERA_FAB_Y_FRACTION]?.coerceIn(0f, 1f) ?: 1f,
|
.map { prefs ->
|
||||||
gallerySortAscending = prefs[Keys.GALLERY_SORT_ASCENDING] ?: true,
|
UserPreferences(
|
||||||
galleryGridMode = prefs[Keys.GALLERY_GRID_MODE] ?: true,
|
rootFolderUri = prefs[Keys.ROOT_FOLDER_URI],
|
||||||
zipJpegCompression = prefs[Keys.ZIP_JPEG_COMPRESSION] ?: true,
|
cameraFabXFraction = prefs[Keys.CAMERA_FAB_X_FRACTION]?.coerceIn(0f, 1f) ?: 1f,
|
||||||
screenTransitionsEnabled = prefs[Keys.SCREEN_TRANSITIONS_ENABLED] ?: true,
|
cameraFabYFraction = prefs[Keys.CAMERA_FAB_Y_FRACTION]?.coerceIn(0f, 1f) ?: 1f,
|
||||||
|
gallerySortAscending = prefs[Keys.GALLERY_SORT_ASCENDING] ?: true,
|
||||||
|
galleryGridMode = prefs[Keys.GALLERY_GRID_MODE] ?: true,
|
||||||
|
zipJpegCompression = prefs[Keys.ZIP_JPEG_COMPRESSION] ?: true,
|
||||||
|
screenTransitionsEnabled = prefs[Keys.SCREEN_TRANSITIONS_ENABLED] ?: true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
scope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
initialValue = UserPreferences(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun setRootFolderUri(uri: String) {
|
suspend fun setRootFolderUri(uri: String) {
|
||||||
context.dataStore.edit { prefs ->
|
context.dataStore.edit { prefs ->
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ import kotlinx.serialization.Serializable
|
|||||||
object FoldersRoute
|
object FoldersRoute
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GalleryRoute(val folderUri: String)
|
data class GalleryRoute(
|
||||||
|
val folderUri: String,
|
||||||
|
val folderName: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CameraRoute(
|
data class CameraRoute(
|
||||||
@@ -64,22 +67,23 @@ fun OneCameraNavGraph(navController: NavHostController) {
|
|||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = FoldersRoute,
|
startDestination = FoldersRoute,
|
||||||
enterTransition = { if (transitionsEnabled) fadeIn(tween(250)) else EnterTransition.None },
|
enterTransition = { if (transitionsEnabled) fadeIn(tween(250)) else EnterTransition.None },
|
||||||
exitTransition = { if (transitionsEnabled) fadeOut(tween(250)) else ExitTransition.None },
|
exitTransition = {
|
||||||
popEnterTransition = { if (transitionsEnabled) fadeIn(tween(250)) else EnterTransition.None },
|
if (transitionsEnabled) {
|
||||||
|
ExitTransition.KeepUntilTransitionsFinished
|
||||||
|
} else {
|
||||||
|
ExitTransition.None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
popEnterTransition = { EnterTransition.None },
|
||||||
popExitTransition = { if (transitionsEnabled) fadeOut(tween(250)) else ExitTransition.None },
|
popExitTransition = { if (transitionsEnabled) fadeOut(tween(250)) else ExitTransition.None },
|
||||||
) {
|
) {
|
||||||
composable<FoldersRoute> {
|
composable<FoldersRoute> {
|
||||||
FoldersScreen(
|
FoldersScreen(
|
||||||
onNavigateToGallery = { folderUri ->
|
onNavigateToGallery = { folderUri, folderName ->
|
||||||
navController.navigate(GalleryRoute(folderUri))
|
navController.navigate(GalleryRoute(folderUri, folderName))
|
||||||
},
|
},
|
||||||
onNavigateToCamera = { folderUri ->
|
onNavigateToCamera = { folderUri ->
|
||||||
navController.navigate(
|
navController.navigate(CameraRoute(targetFolderUri = folderUri))
|
||||||
CameraRoute(
|
|
||||||
targetFolderUri = folderUri,
|
|
||||||
showTargetGalleryAfterSave = true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
onNavigateToSettings = {
|
onNavigateToSettings = {
|
||||||
navController.navigate(SettingsRoute)
|
navController.navigate(SettingsRoute)
|
||||||
@@ -93,8 +97,8 @@ fun OneCameraNavGraph(navController: NavHostController) {
|
|||||||
onNavigateToCamera = { folderUri ->
|
onNavigateToCamera = { folderUri ->
|
||||||
navController.navigate(CameraRoute(folderUri))
|
navController.navigate(CameraRoute(folderUri))
|
||||||
},
|
},
|
||||||
onNavigateToSubfolder = { folderUri ->
|
onNavigateToSubfolder = { folderUri, folderName ->
|
||||||
navController.navigate(GalleryRoute(folderUri))
|
navController.navigate(GalleryRoute(folderUri, folderName))
|
||||||
},
|
},
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ import androidx.lifecycle.compose.LifecycleResumeEffect
|
|||||||
import com.example.onecamera.R
|
import com.example.onecamera.R
|
||||||
import com.example.onecamera.ui.components.OneCameraEmptyState
|
import com.example.onecamera.ui.components.OneCameraEmptyState
|
||||||
import com.example.onecamera.ui.components.OneCameraScreenPadding
|
import com.example.onecamera.ui.components.OneCameraScreenPadding
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -111,23 +113,26 @@ fun SystemCameraCapture(
|
|||||||
LaunchedEffect(hasLaunchedCamera) {
|
LaunchedEffect(hasLaunchedCamera) {
|
||||||
if (!hasLaunchedCamera) {
|
if (!hasLaunchedCamera) {
|
||||||
viewModel.clearLastSaveResult()
|
viewModel.clearLastSaveResult()
|
||||||
val tempDir = File(context.cacheDir, "camera_captures").apply { mkdirs() }
|
val (preparedPath, preparedUri) = withContext(Dispatchers.IO) {
|
||||||
|
val tempDir = File(context.cacheDir, "camera_captures").apply { mkdirs() }
|
||||||
|
|
||||||
tempDir.listFiles()?.forEach { runCatching { it.delete() } }
|
tempDir.listFiles()?.forEach { runCatching { it.delete() } }
|
||||||
val tempFile = File(tempDir, "capture_${System.currentTimeMillis()}.jpg").apply {
|
val tempFile = File(tempDir, "capture_${System.currentTimeMillis()}.jpg").apply {
|
||||||
createNewFile()
|
createNewFile()
|
||||||
|
}
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.fileprovider",
|
||||||
|
tempFile,
|
||||||
|
)
|
||||||
|
tempFile.absolutePath to uri.toString()
|
||||||
}
|
}
|
||||||
val uri = FileProvider.getUriForFile(
|
tempPhotoUriString = preparedUri
|
||||||
context,
|
tempPhotoPath = preparedPath
|
||||||
"${context.packageName}.fileprovider",
|
|
||||||
tempFile,
|
|
||||||
)
|
|
||||||
tempPhotoUriString = uri.toString()
|
|
||||||
tempPhotoPath = tempFile.absolutePath
|
|
||||||
hasLaunchedCamera = true
|
hasLaunchedCamera = true
|
||||||
hasReceivedCameraResult = false
|
hasReceivedCameraResult = false
|
||||||
launchUptimeMillis = SystemClock.elapsedRealtime()
|
launchUptimeMillis = SystemClock.elapsedRealtime()
|
||||||
takePictureLauncher.launch(uri)
|
takePictureLauncher.launch(Uri.parse(preparedUri))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +148,7 @@ fun SystemCameraCapture(
|
|||||||
tempPhotoUriString != null
|
tempPhotoUriString != null
|
||||||
) {
|
) {
|
||||||
val tempLength = tempPhotoPath?.let { path ->
|
val tempLength = tempPhotoPath?.let { path ->
|
||||||
runCatching { File(path).length() }.getOrNull()
|
runCatching { withContext(Dispatchers.IO) { File(path).length() } }.getOrNull()
|
||||||
} ?: 0L
|
} ?: 0L
|
||||||
val tempPhotoUri = tempPhotoUriString?.let(Uri::parse)
|
val tempPhotoUri = tempPhotoUriString?.let(Uri::parse)
|
||||||
if (tempLength > 0L && tempPhotoUri != null) {
|
if (tempLength > 0L && tempPhotoUri != null) {
|
||||||
|
|||||||
@@ -184,10 +184,10 @@ private fun FolderPreview(photos: List<Uri>, statsLoaded: Boolean, modifier: Mod
|
|||||||
if (photos.isEmpty()) {
|
if (photos.isEmpty()) {
|
||||||
|
|
||||||
if (statsLoaded) {
|
if (statsLoaded) {
|
||||||
Icon(
|
OneCameraIconBadge(
|
||||||
painter = painterResource(R.drawable.ic_symbol_folder),
|
icon = painterResource(R.drawable.ic_symbol_photo_library),
|
||||||
contentDescription = null,
|
size = 64.dp,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
iconSize = 32.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -19,6 +20,7 @@ import androidx.compose.material3.ListItem
|
|||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -50,8 +52,11 @@ fun FolderTreePickerSheet(
|
|||||||
val selectedFolder = state.folders.firstOrNull { it.uri.toString() == selectedFolderUri }
|
val selectedFolder = state.folders.firstOrNull { it.uri.toString() == selectedFolderUri }
|
||||||
val canMove = selectedFolder != null && !state.isMoving
|
val canMove = selectedFolder != null && !state.isMoving
|
||||||
|
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = sheetState,
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -105,7 +110,11 @@ fun FolderTreePickerSheet(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f, fill = false),
|
.weight(1f, fill = false),
|
||||||
) {
|
) {
|
||||||
items(state.folders, key = { it.uri }) { folder ->
|
items(
|
||||||
|
items = state.folders,
|
||||||
|
key = { it.uri },
|
||||||
|
contentType = { "folder" },
|
||||||
|
) { folder ->
|
||||||
val isCurrent = currentFolderUri == folder.uri.toString()
|
val isCurrent = currentFolderUri == folder.uri.toString()
|
||||||
val isSelected = selectedFolderUri == folder.uri.toString()
|
val isSelected = selectedFolderUri == folder.uri.toString()
|
||||||
ListItem(
|
ListItem(
|
||||||
|
|||||||
@@ -1,50 +1,113 @@
|
|||||||
package com.example.onecamera.ui.folders
|
package com.example.onecamera.ui.folders
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextLayoutResult
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.example.onecamera.R
|
import com.example.onecamera.R
|
||||||
import com.example.onecamera.domain.model.FolderNode
|
import com.example.onecamera.domain.model.FolderNode
|
||||||
import com.example.onecamera.domain.model.Photo
|
import com.example.onecamera.domain.model.Photo
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NameField(
|
||||||
|
label: String,
|
||||||
|
state: TextFieldState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
val density = LocalDensity.current
|
||||||
|
var layoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||||
|
var fieldWidth by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
LaunchedEffect(state.selection, layoutResult, fieldWidth) {
|
||||||
|
val layout = layoutResult ?: return@LaunchedEffect
|
||||||
|
if (fieldWidth == 0) return@LaunchedEffect
|
||||||
|
val cursor = state.selection.end.coerceIn(0, layout.layoutInput.text.length)
|
||||||
|
val cursorX = layout.getHorizontalPosition(cursor, true).coerceAtLeast(0f)
|
||||||
|
val edgeMargin = with(density) { 40.dp.toPx() }
|
||||||
|
val target = when {
|
||||||
|
cursorX < scrollState.value -> cursorX
|
||||||
|
cursorX > scrollState.value + fieldWidth - edgeMargin ->
|
||||||
|
cursorX - fieldWidth + edgeMargin
|
||||||
|
else -> return@LaunchedEffect
|
||||||
|
}
|
||||||
|
scrollState.animateScrollTo(target.roundToInt().coerceIn(0, scrollState.maxValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
BasicTextField(
|
||||||
|
state = state,
|
||||||
|
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
onTextLayout = { textLayoutResultProvider ->
|
||||||
|
layoutResult = textLayoutResultProvider()
|
||||||
|
},
|
||||||
|
scrollState = scrollState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onSizeChanged { fieldWidth = it.width }
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.outline,
|
||||||
|
MaterialTheme.shapes.extraSmall,
|
||||||
|
)
|
||||||
|
.padding(horizontal = 14.dp, vertical = 14.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CreateFolderDialog(
|
fun CreateFolderDialog(
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onCreate: (String) -> Unit,
|
onCreate: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf("") }
|
val nameState = rememberTextFieldState("")
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(stringResource(R.string.dialog_create_folder_title)) },
|
title = { Text(stringResource(R.string.dialog_create_folder_title)) },
|
||||||
text = {
|
text = {
|
||||||
OutlinedTextField(
|
NameField(
|
||||||
value = name,
|
label = stringResource(R.string.dialog_folder_name),
|
||||||
onValueChange = { name = it },
|
state = nameState,
|
||||||
label = { Text(stringResource(R.string.dialog_folder_name)) },
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { onCreate(name) },
|
onClick = { onCreate(nameState.text.toString()) },
|
||||||
enabled = name.isNotBlank(),
|
enabled = nameState.text.isNotBlank(),
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.action_create))
|
Text(stringResource(R.string.action_create))
|
||||||
}
|
}
|
||||||
@@ -63,7 +126,10 @@ fun RenameFolderDialog(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onRename: (String) -> Unit,
|
onRename: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf(folder.name) }
|
val nameState = rememberTextFieldState(
|
||||||
|
initialText = folder.name,
|
||||||
|
initialSelection = TextRange(folder.name.length),
|
||||||
|
)
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
@@ -72,19 +138,16 @@ fun RenameFolderDialog(
|
|||||||
Column {
|
Column {
|
||||||
Text(stringResource(R.string.dialog_current_name, folder.name))
|
Text(stringResource(R.string.dialog_current_name, folder.name))
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
OutlinedTextField(
|
NameField(
|
||||||
value = name,
|
label = stringResource(R.string.dialog_new_name),
|
||||||
onValueChange = { name = it },
|
state = nameState,
|
||||||
label = { Text(stringResource(R.string.dialog_new_name)) },
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { onRename(name) },
|
onClick = { onRename(nameState.text.toString()) },
|
||||||
enabled = name.isNotBlank() && name != folder.name,
|
enabled = nameState.text.isNotBlank() && nameState.text.toString() != folder.name,
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.action_rename))
|
Text(stringResource(R.string.action_rename))
|
||||||
}
|
}
|
||||||
@@ -103,7 +166,10 @@ fun RenameFileDialog(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onRename: (String) -> Unit,
|
onRename: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf(file.name) }
|
val nameState = rememberTextFieldState(
|
||||||
|
initialText = file.name,
|
||||||
|
initialSelection = TextRange(file.name.length),
|
||||||
|
)
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
@@ -112,19 +178,16 @@ fun RenameFileDialog(
|
|||||||
Column {
|
Column {
|
||||||
Text(stringResource(R.string.dialog_current_name, file.name))
|
Text(stringResource(R.string.dialog_current_name, file.name))
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
OutlinedTextField(
|
NameField(
|
||||||
value = name,
|
label = stringResource(R.string.dialog_new_name),
|
||||||
onValueChange = { name = it },
|
state = nameState,
|
||||||
label = { Text(stringResource(R.string.dialog_new_name)) },
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { onRename(name) },
|
onClick = { onRename(nameState.text.toString()) },
|
||||||
enabled = name.isNotBlank() && name != file.name,
|
enabled = nameState.text.isNotBlank() && nameState.text.toString() != file.name,
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.action_rename))
|
Text(stringResource(R.string.action_rename))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import androidx.compose.foundation.lazy.grid.GridCells
|
|||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -27,6 +28,7 @@ import androidx.compose.material3.SnackbarHost
|
|||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -49,15 +51,17 @@ import androidx.lifecycle.compose.LifecycleResumeEffect
|
|||||||
import com.example.onecamera.R
|
import com.example.onecamera.R
|
||||||
import com.example.onecamera.ui.components.DraggableCameraFab
|
import com.example.onecamera.ui.components.DraggableCameraFab
|
||||||
import com.example.onecamera.ui.components.FolderCard
|
import com.example.onecamera.ui.components.FolderCard
|
||||||
|
import com.example.onecamera.ui.components.FolderTreePickerSheet
|
||||||
import com.example.onecamera.ui.components.OneCameraContentMaxWidth
|
import com.example.onecamera.ui.components.OneCameraContentMaxWidth
|
||||||
import com.example.onecamera.ui.components.OneCameraEmptyState
|
import com.example.onecamera.ui.components.OneCameraEmptyState
|
||||||
import com.example.onecamera.ui.components.OneCameraScreenPadding
|
import com.example.onecamera.ui.components.OneCameraScreenPadding
|
||||||
|
import com.example.onecamera.ui.components.PhotoCard
|
||||||
import com.example.onecamera.ui.components.oneCameraTopAppBarColors
|
import com.example.onecamera.ui.components.oneCameraTopAppBarColors
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun FoldersScreen(
|
fun FoldersScreen(
|
||||||
onNavigateToGallery: (String) -> Unit,
|
onNavigateToGallery: (folderUri: String, folderName: String) -> Unit,
|
||||||
onNavigateToCamera: (String) -> Unit,
|
onNavigateToCamera: (String) -> Unit,
|
||||||
onNavigateToSettings: () -> Unit,
|
onNavigateToSettings: () -> Unit,
|
||||||
viewModel: FoldersViewModel = hiltViewModel(),
|
viewModel: FoldersViewModel = hiltViewModel(),
|
||||||
@@ -192,8 +196,8 @@ fun FoldersScreen(
|
|||||||
.widthIn(max = 560.dp),
|
.widthIn(max = 560.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
uiState.isLoading && uiState.folders.isEmpty() -> Unit
|
uiState.isLoading && uiState.folders.isEmpty() && uiState.files.isEmpty() -> Unit
|
||||||
uiState.folders.isEmpty() -> {
|
uiState.folders.isEmpty() && uiState.files.isEmpty() -> {
|
||||||
OneCameraEmptyState(
|
OneCameraEmptyState(
|
||||||
icon = painterResource(R.drawable.ic_symbol_create_new_folder),
|
icon = painterResource(R.drawable.ic_symbol_create_new_folder),
|
||||||
title = stringResource(R.string.folders_empty_title),
|
title = stringResource(R.string.folders_empty_title),
|
||||||
@@ -222,7 +226,7 @@ fun FoldersScreen(
|
|||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = uiState.folders,
|
items = uiState.folders,
|
||||||
key = { it.uri },
|
key = { "folder_${it.uri}" },
|
||||||
contentType = { "folder" },
|
contentType = { "folder" },
|
||||||
) { folder ->
|
) { folder ->
|
||||||
FolderCard(
|
FolderCard(
|
||||||
@@ -230,16 +234,33 @@ fun FoldersScreen(
|
|||||||
|
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.prefetchFolderForNavigation(folder.uri.toString())
|
viewModel.prefetchFolderForNavigation(folder.uri.toString())
|
||||||
onNavigateToGallery(folder.uri.toString())
|
onNavigateToGallery(folder.uri.toString(), folder.name)
|
||||||
},
|
},
|
||||||
onCompress = { viewModel.zipFolderAndShare(folder) },
|
onCompress = { viewModel.zipFolderAndShare(folder) },
|
||||||
onCompressJpeg85 = {
|
onCompressJpeg85 = {
|
||||||
viewModel.zipFolderCompressedJpegsAndShare(folder)
|
viewModel.zipFolderCompressedJpegsAndShare(folder)
|
||||||
},
|
},
|
||||||
|
onMove = { viewModel.showMoveSheetForFolder(folder) },
|
||||||
onRename = { viewModel.showRenameDialog(folder) },
|
onRename = { viewModel.showRenameDialog(folder) },
|
||||||
onDelete = { viewModel.showDeleteDialog(folder) },
|
onDelete = { viewModel.showDeleteDialog(folder) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
items(
|
||||||
|
items = uiState.files,
|
||||||
|
key = { "file_${it.uri}" },
|
||||||
|
contentType = { "file" },
|
||||||
|
) { file ->
|
||||||
|
PhotoCard(
|
||||||
|
photo = file,
|
||||||
|
onClick = { },
|
||||||
|
onMove = { viewModel.showMoveSheetForFile(file) },
|
||||||
|
onRename = { viewModel.showRenameFileDialog(file) },
|
||||||
|
onCompressFolder = { viewModel.zipFileAndShare(file) },
|
||||||
|
onCompressJpeg85 = { viewModel.zipFileCompressedJpegsAndShare(file) },
|
||||||
|
onShare = { viewModel.shareFile(file) },
|
||||||
|
onDelete = { viewModel.showDeleteFileDialog(file) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,4 +298,43 @@ fun FoldersScreen(
|
|||||||
onDelete = { viewModel.deleteFolder(folder) },
|
onDelete = { viewModel.deleteFolder(folder) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
uiState.renameFile?.let { file ->
|
||||||
|
RenameFileDialog(
|
||||||
|
file = file,
|
||||||
|
onDismiss = viewModel::dismissRenameFileDialog,
|
||||||
|
onRename = { viewModel.renameFile(file, it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
uiState.deleteFile?.let { file ->
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = viewModel::dismissDeleteFileDialog,
|
||||||
|
title = { Text(stringResource(R.string.gallery_delete_photo_title)) },
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.gallery_delete_photo_message, file.name))
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { viewModel.deleteFile(file) }) {
|
||||||
|
Text(stringResource(R.string.action_delete))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = viewModel::dismissDeleteFileDialog) {
|
||||||
|
Text(stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
uiState.moveSheet?.let { sheet ->
|
||||||
|
FolderTreePickerSheet(
|
||||||
|
state = sheet,
|
||||||
|
currentFolderUri = uiState.rootFolderUri,
|
||||||
|
onDismiss = viewModel::dismissMoveSheet,
|
||||||
|
onSelect = { target ->
|
||||||
|
when {
|
||||||
|
sheet.folder != null -> viewModel.moveFolderTo(sheet.folder, target)
|
||||||
|
sheet.file != null -> viewModel.moveFileTo(sheet.file, target)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -13,14 +13,18 @@ import com.example.onecamera.data.storage.ExternalMediaObserver
|
|||||||
import com.example.onecamera.data.storage.saf.MissingUriPermissionException
|
import com.example.onecamera.data.storage.saf.MissingUriPermissionException
|
||||||
import com.example.onecamera.domain.model.FolderNode
|
import com.example.onecamera.domain.model.FolderNode
|
||||||
import com.example.onecamera.domain.model.GalleryFileKind
|
import com.example.onecamera.domain.model.GalleryFileKind
|
||||||
|
import com.example.onecamera.domain.model.MovePhotoResult
|
||||||
import com.example.onecamera.domain.model.Photo
|
import com.example.onecamera.domain.model.Photo
|
||||||
import com.example.onecamera.domain.model.mergeLoadedSubfolderStats
|
import com.example.onecamera.domain.model.mergeLoadedSubfolderStats
|
||||||
import com.example.onecamera.domain.repository.PhotoRepository
|
import com.example.onecamera.domain.repository.PhotoRepository
|
||||||
import com.example.onecamera.domain.usecase.CreateFolderUseCase
|
import com.example.onecamera.domain.usecase.CreateFolderUseCase
|
||||||
import com.example.onecamera.domain.usecase.DeleteEntryUseCase
|
import com.example.onecamera.domain.usecase.DeleteEntryUseCase
|
||||||
|
import com.example.onecamera.domain.usecase.ListFolderTreeUseCase
|
||||||
import com.example.onecamera.domain.usecase.ListGalleryUseCase
|
import com.example.onecamera.domain.usecase.ListGalleryUseCase
|
||||||
|
import com.example.onecamera.domain.usecase.MovePhotoUseCase
|
||||||
import com.example.onecamera.domain.usecase.RenameEntryUseCase
|
import com.example.onecamera.domain.usecase.RenameEntryUseCase
|
||||||
import com.example.onecamera.domain.usecase.ZipFolderUseCase
|
import com.example.onecamera.domain.usecase.ZipFolderUseCase
|
||||||
|
import com.example.onecamera.ui.gallery.MovePhotoSheetState
|
||||||
import com.example.onecamera.ui.components.evictThumbnails
|
import com.example.onecamera.ui.components.evictThumbnails
|
||||||
import com.example.onecamera.ui.components.thumbnailWarmupRequests
|
import com.example.onecamera.ui.components.thumbnailWarmupRequests
|
||||||
import com.example.onecamera.ui.components.NAVIGATION_PREFETCH_THUMBNAIL_COUNT
|
import com.example.onecamera.ui.components.NAVIGATION_PREFETCH_THUMBNAIL_COUNT
|
||||||
@@ -35,12 +39,16 @@ import javax.inject.Inject
|
|||||||
@Immutable
|
@Immutable
|
||||||
data class FoldersUiState(
|
data class FoldersUiState(
|
||||||
val folders: List<FolderNode> = emptyList(),
|
val folders: List<FolderNode> = emptyList(),
|
||||||
|
val files: List<Photo> = emptyList(),
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val rootFolderUri: String? = null,
|
val rootFolderUri: String? = null,
|
||||||
val isRootFolderResolved: Boolean = false,
|
val isRootFolderResolved: Boolean = false,
|
||||||
val showCreateDialog: Boolean = false,
|
val showCreateDialog: Boolean = false,
|
||||||
val showRenameDialog: FolderNode? = null,
|
val showRenameDialog: FolderNode? = null,
|
||||||
val showDeleteDialog: FolderNode? = null,
|
val showDeleteDialog: FolderNode? = null,
|
||||||
|
val renameFile: Photo? = null,
|
||||||
|
val deleteFile: Photo? = null,
|
||||||
|
val moveSheet: MovePhotoSheetState? = null,
|
||||||
val cameraFabXFraction: Float = 1f,
|
val cameraFabXFraction: Float = 1f,
|
||||||
val cameraFabYFraction: Float = 1f,
|
val cameraFabYFraction: Float = 1f,
|
||||||
val shareFileRequest: com.example.onecamera.ui.gallery.ShareFileRequest? = null,
|
val shareFileRequest: com.example.onecamera.ui.gallery.ShareFileRequest? = null,
|
||||||
@@ -54,6 +62,8 @@ class FoldersViewModel @Inject constructor(
|
|||||||
private val preferencesRepository: UserPreferencesRepository,
|
private val preferencesRepository: UserPreferencesRepository,
|
||||||
private val photoRepository: PhotoRepository,
|
private val photoRepository: PhotoRepository,
|
||||||
private val listGalleryUseCase: ListGalleryUseCase,
|
private val listGalleryUseCase: ListGalleryUseCase,
|
||||||
|
private val listFolderTreeUseCase: ListFolderTreeUseCase,
|
||||||
|
private val movePhotoUseCase: MovePhotoUseCase,
|
||||||
private val createFolderUseCase: CreateFolderUseCase,
|
private val createFolderUseCase: CreateFolderUseCase,
|
||||||
private val renameEntryUseCase: RenameEntryUseCase,
|
private val renameEntryUseCase: RenameEntryUseCase,
|
||||||
private val deleteEntryUseCase: DeleteEntryUseCase,
|
private val deleteEntryUseCase: DeleteEntryUseCase,
|
||||||
@@ -123,13 +133,14 @@ class FoldersViewModel @Inject constructor(
|
|||||||
val uri = rootUriString.toUri()
|
val uri = rootUriString.toUri()
|
||||||
runCatching {
|
runCatching {
|
||||||
OneCameraTrace.asyncSection(OneCameraTrace.FOLDERS_LOAD) {
|
OneCameraTrace.asyncSection(OneCameraTrace.FOLDERS_LOAD) {
|
||||||
listGalleryUseCase(uri).subfolders
|
listGalleryUseCase(uri)
|
||||||
}
|
}
|
||||||
}.onSuccess { folders ->
|
}.onSuccess { content ->
|
||||||
|
|
||||||
val merged = mergeLoadedSubfolderStats(folders, uiState.value.folders)
|
val merged = mergeLoadedSubfolderStats(content.subfolders, uiState.value.folders)
|
||||||
uiState.value = uiState.value.copy(
|
uiState.value = uiState.value.copy(
|
||||||
folders = merged,
|
folders = merged,
|
||||||
|
files = content.photos,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
error = null,
|
error = null,
|
||||||
needsRootReselection = false,
|
needsRootReselection = false,
|
||||||
@@ -219,6 +230,10 @@ class FoldersViewModel @Inject constructor(
|
|||||||
statsLoaded = true,
|
statsLoaded = true,
|
||||||
)
|
)
|
||||||
uiState.value = uiState.value.copy(folders = updated)
|
uiState.value = uiState.value.copy(folders = updated)
|
||||||
|
} else if (!old.statsLoaded) {
|
||||||
|
val updated = current.toMutableList()
|
||||||
|
updated[idx] = old.copy(statsLoaded = true)
|
||||||
|
uiState.value = uiState.value.copy(folders = updated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,6 +271,9 @@ class FoldersViewModel @Inject constructor(
|
|||||||
statsLoaded = true,
|
statsLoaded = true,
|
||||||
)
|
)
|
||||||
changed = true
|
changed = true
|
||||||
|
} else if (!old.statsLoaded) {
|
||||||
|
updated[idx] = old.copy(statsLoaded = true)
|
||||||
|
changed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,6 +385,230 @@ class FoldersViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun zipFileAndShare(file: Photo) {
|
||||||
|
val rootUri = uiState.value.rootFolderUri?.toUri() ?: return
|
||||||
|
viewModelScope.launch {
|
||||||
|
uiState.value = uiState.value.copy(error = null)
|
||||||
|
val archiveBase = file.name.substringBeforeLast('.').ifBlank { file.name }
|
||||||
|
val outcome = zipFolderUseCase.selectedFiles(rootUri, archiveBase, listOf(file))
|
||||||
|
if (outcome.success && outcome.archive != null) {
|
||||||
|
uiState.value = uiState.value.copy(
|
||||||
|
shareFileRequest = outcome.archive.toShareFileRequest(),
|
||||||
|
)
|
||||||
|
photoEventBus.onPhotoSaved(rootUri, outcome.archive)
|
||||||
|
loadFolders()
|
||||||
|
} else {
|
||||||
|
uiState.value = uiState.value.copy(
|
||||||
|
error = outcome.errorMessage ?: "Could not create ZIP archive.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun zipFileCompressedJpegsAndShare(file: Photo) {
|
||||||
|
val rootUri = uiState.value.rootFolderUri?.toUri() ?: return
|
||||||
|
viewModelScope.launch {
|
||||||
|
uiState.value = uiState.value.copy(error = null)
|
||||||
|
val archiveBase = file.name.substringBeforeLast('.').ifBlank { file.name }
|
||||||
|
val outcome = zipFolderUseCase.compressedSelectedJpegs(rootUri, archiveBase, listOf(file))
|
||||||
|
if (outcome.success && outcome.archive != null) {
|
||||||
|
uiState.value = uiState.value.copy(
|
||||||
|
shareFileRequest = outcome.archive.toShareFileRequest(),
|
||||||
|
)
|
||||||
|
photoEventBus.onPhotoSaved(rootUri, outcome.archive)
|
||||||
|
loadFolders()
|
||||||
|
} else {
|
||||||
|
uiState.value = uiState.value.copy(
|
||||||
|
error = outcome.errorMessage ?: "Could not create ZIP archive.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shareFile(file: Photo) {
|
||||||
|
uiState.value = uiState.value.copy(shareFileRequest = file.toShareFileRequest())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showRenameFileDialog(file: Photo) {
|
||||||
|
uiState.value = uiState.value.copy(renameFile = file)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissRenameFileDialog() {
|
||||||
|
uiState.value = uiState.value.copy(renameFile = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun renameFile(file: Photo, newName: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val renamed = renameEntryUseCase(file.uri, newName)
|
||||||
|
dismissRenameFileDialog()
|
||||||
|
if (!renamed) {
|
||||||
|
uiState.value = uiState.value.copy(error = "Failed to rename file")
|
||||||
|
}
|
||||||
|
loadFolders()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showDeleteFileDialog(file: Photo) {
|
||||||
|
uiState.value = uiState.value.copy(deleteFile = file)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissDeleteFileDialog() {
|
||||||
|
uiState.value = uiState.value.copy(deleteFile = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteFile(file: Photo) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val deleted = deleteEntryUseCase(file.uri)
|
||||||
|
dismissDeleteFileDialog()
|
||||||
|
if (!deleted) {
|
||||||
|
uiState.value = uiState.value.copy(error = "Failed to delete file")
|
||||||
|
}
|
||||||
|
loadFolders()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showMoveSheetForFolder(folder: FolderNode) {
|
||||||
|
uiState.value = uiState.value.copy(
|
||||||
|
moveSheet = MovePhotoSheetState(folder = folder),
|
||||||
|
)
|
||||||
|
loadMoveDestinations(
|
||||||
|
onMissingRoot = {
|
||||||
|
MovePhotoSheetState(
|
||||||
|
folder = folder,
|
||||||
|
isLoading = false,
|
||||||
|
error = "Select a root folder in Settings first.",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onLoaded = { destinationFolders ->
|
||||||
|
val movingPath = folder.path
|
||||||
|
uiState.value.moveSheet?.copy(
|
||||||
|
folders = destinationFolders.filterNot { destination ->
|
||||||
|
destination.uri == folder.uri ||
|
||||||
|
destination.path == movingPath ||
|
||||||
|
destination.path.startsWith("$movingPath/")
|
||||||
|
},
|
||||||
|
isLoading = false,
|
||||||
|
error = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showMoveSheetForFile(file: Photo) {
|
||||||
|
uiState.value = uiState.value.copy(
|
||||||
|
moveSheet = MovePhotoSheetState(file = file),
|
||||||
|
)
|
||||||
|
loadMoveDestinations(
|
||||||
|
onMissingRoot = {
|
||||||
|
MovePhotoSheetState(
|
||||||
|
file = file,
|
||||||
|
isLoading = false,
|
||||||
|
error = "Select a root folder in Settings first.",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onLoaded = { destinationFolders ->
|
||||||
|
uiState.value.moveSheet?.copy(
|
||||||
|
folders = destinationFolders,
|
||||||
|
isLoading = false,
|
||||||
|
error = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissMoveSheet() {
|
||||||
|
uiState.value = uiState.value.copy(moveSheet = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadMoveDestinations(
|
||||||
|
onMissingRoot: () -> MovePhotoSheetState,
|
||||||
|
onLoaded: (List<FolderNode>) -> MovePhotoSheetState?,
|
||||||
|
) {
|
||||||
|
val rootUri = uiState.value.rootFolderUri?.toUri()
|
||||||
|
if (rootUri == null) {
|
||||||
|
uiState.value = uiState.value.copy(moveSheet = onMissingRoot())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatching {
|
||||||
|
listFolderTreeUseCase(rootUri)
|
||||||
|
}.onSuccess { folders ->
|
||||||
|
uiState.value = uiState.value.copy(
|
||||||
|
moveSheet = onLoaded(folders) ?: uiState.value.moveSheet?.copy(
|
||||||
|
folders = folders,
|
||||||
|
isLoading = false,
|
||||||
|
error = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}.onFailure { error ->
|
||||||
|
uiState.value = uiState.value.copy(
|
||||||
|
moveSheet = uiState.value.moveSheet?.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = error.message ?: "Failed to load folder tree",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveFolderTo(folder: FolderNode, targetFolder: FolderNode) {
|
||||||
|
val rootUri = uiState.value.rootFolderUri?.toUri() ?: return
|
||||||
|
viewModelScope.launch {
|
||||||
|
uiState.value = uiState.value.copy(
|
||||||
|
moveSheet = uiState.value.moveSheet?.copy(isMoving = true, error = null),
|
||||||
|
)
|
||||||
|
when (val result = movePhotoUseCase.folder(folder.uri, rootUri, targetFolder.uri, folder.name)) {
|
||||||
|
is MovePhotoResult.Success -> {
|
||||||
|
uiState.value = uiState.value.copy(moveSheet = null)
|
||||||
|
photoEventBus.onPhotoSaved(targetFolder.uri)
|
||||||
|
photoEventBus.onPhotoSaved(rootUri)
|
||||||
|
loadFolders()
|
||||||
|
}
|
||||||
|
is MovePhotoResult.PartialSuccess -> {
|
||||||
|
uiState.value = uiState.value.copy(moveSheet = null, error = result.message)
|
||||||
|
loadFolders()
|
||||||
|
}
|
||||||
|
is MovePhotoResult.Failure -> {
|
||||||
|
uiState.value = uiState.value.copy(
|
||||||
|
moveSheet = uiState.value.moveSheet?.copy(
|
||||||
|
isMoving = false,
|
||||||
|
error = result.message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveFileTo(file: Photo, targetFolder: FolderNode) {
|
||||||
|
val rootUri = uiState.value.rootFolderUri?.toUri() ?: return
|
||||||
|
viewModelScope.launch {
|
||||||
|
uiState.value = uiState.value.copy(
|
||||||
|
moveSheet = uiState.value.moveSheet?.copy(isMoving = true, error = null),
|
||||||
|
)
|
||||||
|
when (val result = movePhotoUseCase(file.uri, rootUri, targetFolder.uri, file.name)) {
|
||||||
|
is MovePhotoResult.Success -> {
|
||||||
|
uiState.value = uiState.value.copy(moveSheet = null)
|
||||||
|
photoEventBus.onPhotoSaved(targetFolder.uri)
|
||||||
|
photoEventBus.onPhotoSaved(rootUri)
|
||||||
|
loadFolders()
|
||||||
|
}
|
||||||
|
is MovePhotoResult.PartialSuccess -> {
|
||||||
|
uiState.value = uiState.value.copy(moveSheet = null, error = result.message)
|
||||||
|
loadFolders()
|
||||||
|
}
|
||||||
|
is MovePhotoResult.Failure -> {
|
||||||
|
uiState.value = uiState.value.copy(
|
||||||
|
moveSheet = uiState.value.moveSheet?.copy(
|
||||||
|
isMoving = false,
|
||||||
|
error = result.message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun clearShareRequest() {
|
fun clearShareRequest() {
|
||||||
uiState.value = uiState.value.copy(shareFileRequest = null)
|
uiState.value = uiState.value.copy(shareFileRequest = null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -41,11 +42,11 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -86,13 +87,14 @@ import com.example.onecamera.ui.folders.RenameFolderDialog
|
|||||||
import com.github.panpf.zoomimage.CoilZoomAsyncImage
|
import com.github.panpf.zoomimage.CoilZoomAsyncImage
|
||||||
import com.github.panpf.zoomimage.rememberCoilZoomState
|
import com.github.panpf.zoomimage.rememberCoilZoomState
|
||||||
import com.github.panpf.zoomimage.zoom.GestureType
|
import com.github.panpf.zoomimage.zoom.GestureType
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun GalleryScreen(
|
fun GalleryScreen(
|
||||||
folderUri: String,
|
folderUri: String,
|
||||||
onNavigateToCamera: (String) -> Unit,
|
onNavigateToCamera: (String) -> Unit,
|
||||||
onNavigateToSubfolder: (String) -> Unit,
|
onNavigateToSubfolder: (folderUri: String, folderName: String) -> Unit,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
viewModel: GalleryViewModel = hiltViewModel(),
|
viewModel: GalleryViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
@@ -198,10 +200,8 @@ fun GalleryScreen(
|
|||||||
uiState.subfolders.sortedByDescending { it.name.lowercase() }
|
uiState.subfolders.sortedByDescending { it.name.lowercase() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val sortedGalleryPhotos by remember(sortedPhotos) {
|
val sortedGalleryPhotos = remember(sortedPhotos) {
|
||||||
derivedStateOf {
|
sortedPhotos.filter { it.kind == GalleryFileKind.Photo }
|
||||||
sortedPhotos.filter { it.kind == GalleryFileKind.Photo }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
when (val dialog = uiState.dialog) {
|
when (val dialog = uiState.dialog) {
|
||||||
@@ -282,9 +282,7 @@ fun GalleryScreen(
|
|||||||
title = {
|
title = {
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
uiState.folderName.ifEmpty {
|
uiState.folderName,
|
||||||
stringResource(R.string.gallery_title)
|
|
||||||
},
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
@@ -334,47 +332,50 @@ fun GalleryScreen(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
val folderHasContent = uiState.photos.isNotEmpty() || uiState.subfolders.isNotEmpty()
|
||||||
ButtonGroup(overflowIndicator = {}) {
|
ButtonGroup(overflowIndicator = {}) {
|
||||||
customItem(
|
if (folderHasContent) {
|
||||||
buttonGroupContent = {
|
customItem(
|
||||||
IconButton(onClick = { viewModel.toggleSortOrder() }) {
|
buttonGroupContent = {
|
||||||
Icon(
|
IconButton(onClick = { viewModel.toggleSortOrder() }) {
|
||||||
painter = painterResource(
|
Icon(
|
||||||
if (uiState.sortAscending) {
|
painter = painterResource(
|
||||||
R.drawable.sort_ascending_24
|
if (uiState.sortAscending) {
|
||||||
|
R.drawable.sort_ascending_24
|
||||||
|
} else {
|
||||||
|
R.drawable.sort_descending_24
|
||||||
|
},
|
||||||
|
),
|
||||||
|
contentDescription = if (uiState.sortAscending) {
|
||||||
|
stringResource(R.string.gallery_sort_descending)
|
||||||
} else {
|
} else {
|
||||||
R.drawable.sort_descending_24
|
stringResource(R.string.gallery_sort_ascending)
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
contentDescription = if (uiState.sortAscending) {
|
}
|
||||||
stringResource(R.string.gallery_sort_descending)
|
},
|
||||||
} else {
|
menuContent = {},
|
||||||
stringResource(R.string.gallery_sort_ascending)
|
)
|
||||||
},
|
customItem(
|
||||||
)
|
buttonGroupContent = {
|
||||||
}
|
IconButton(onClick = { viewModel.toggleDisplayMode() }) {
|
||||||
},
|
Icon(
|
||||||
menuContent = {},
|
painter = if (uiState.isGridMode) {
|
||||||
)
|
painterResource(R.drawable.ic_symbol_view_list)
|
||||||
customItem(
|
} else {
|
||||||
buttonGroupContent = {
|
painterResource(R.drawable.ic_symbol_grid_view)
|
||||||
IconButton(onClick = { viewModel.toggleDisplayMode() }) {
|
},
|
||||||
Icon(
|
contentDescription = if (uiState.isGridMode) {
|
||||||
painter = if (uiState.isGridMode) {
|
stringResource(R.string.gallery_show_list)
|
||||||
painterResource(R.drawable.ic_symbol_view_list)
|
} else {
|
||||||
} else {
|
stringResource(R.string.gallery_show_grid)
|
||||||
painterResource(R.drawable.ic_symbol_grid_view)
|
},
|
||||||
},
|
)
|
||||||
contentDescription = if (uiState.isGridMode) {
|
}
|
||||||
stringResource(R.string.gallery_show_list)
|
},
|
||||||
} else {
|
menuContent = {},
|
||||||
stringResource(R.string.gallery_show_grid)
|
)
|
||||||
},
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
menuContent = {},
|
|
||||||
)
|
|
||||||
customItem(
|
customItem(
|
||||||
buttonGroupContent = {
|
buttonGroupContent = {
|
||||||
IconButton(onClick = { viewModel.showCreateSubfolderDialog() }) {
|
IconButton(onClick = { viewModel.showCreateSubfolderDialog() }) {
|
||||||
@@ -409,14 +410,6 @@ fun GalleryScreen(
|
|||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
when {
|
when {
|
||||||
|
|
||||||
uiState.selectedPhoto != null -> {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(Color.Black),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
!uiState.viewPreferencesLoaded -> {
|
!uiState.viewPreferencesLoaded -> {
|
||||||
Box(modifier = Modifier.fillMaxSize())
|
Box(modifier = Modifier.fillMaxSize())
|
||||||
}
|
}
|
||||||
@@ -443,7 +436,7 @@ fun GalleryScreen(
|
|||||||
|
|
||||||
onFolderClick = {
|
onFolderClick = {
|
||||||
viewModel.prefetchFolderForNavigation(it.uri.toString())
|
viewModel.prefetchFolderForNavigation(it.uri.toString())
|
||||||
onNavigateToSubfolder(it.uri.toString())
|
onNavigateToSubfolder(it.uri.toString(), it.name)
|
||||||
},
|
},
|
||||||
onFolderCompress = { viewModel.zipFolderAndShare(it) },
|
onFolderCompress = { viewModel.zipFolderAndShare(it) },
|
||||||
onFolderCompressJpeg85 = { viewModel.zipFolderCompressedJpegsAndShare(it) },
|
onFolderCompressJpeg85 = { viewModel.zipFolderCompressedJpegsAndShare(it) },
|
||||||
@@ -485,12 +478,14 @@ fun GalleryScreen(
|
|||||||
enter = fadeIn(tween(250)),
|
enter = fadeIn(tween(250)),
|
||||||
exit = fadeOut(tween(150)),
|
exit = fadeOut(tween(150)),
|
||||||
) {
|
) {
|
||||||
DraggableCameraFab(
|
if (uiState.viewPreferencesLoaded) {
|
||||||
xFraction = uiState.cameraFabXFraction,
|
DraggableCameraFab(
|
||||||
yFraction = uiState.cameraFabYFraction,
|
xFraction = uiState.cameraFabXFraction,
|
||||||
onPositionChange = viewModel::saveCameraFabPosition,
|
yFraction = uiState.cameraFabYFraction,
|
||||||
onClick = { onNavigateToCamera(uiState.currentFolderUri ?: folderUri) },
|
onPositionChange = viewModel::saveCameraFabPosition,
|
||||||
)
|
onClick = { onNavigateToCamera(uiState.currentFolderUri ?: folderUri) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectionActionsBar(
|
SelectionActionsBar(
|
||||||
@@ -537,10 +532,8 @@ internal fun FullscreenPhotoPagerContent(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onDelete: (Photo) -> Unit,
|
onDelete: (Photo) -> Unit,
|
||||||
) {
|
) {
|
||||||
val initialPage by remember(photos, initialPhoto.uri) {
|
val initialPage = remember(photos, initialPhoto.uri) {
|
||||||
derivedStateOf {
|
photos.indexOfFirst { it.uri == initialPhoto.uri }.coerceAtLeast(0)
|
||||||
photos.indexOfFirst { it.uri == initialPhoto.uri }.coerceAtLeast(0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val pagerState = rememberPagerState(initialPage = initialPage) { photos.size.coerceAtLeast(1) }
|
val pagerState = rememberPagerState(initialPage = initialPage) { photos.size.coerceAtLeast(1) }
|
||||||
|
|
||||||
@@ -570,19 +563,29 @@ internal fun FullscreenPhotoPagerContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IconButton(
|
Row(
|
||||||
onClick = onDismiss,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
.padding(16.dp),
|
.padding(start = 16.dp, top = 16.dp, end = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Text(
|
||||||
painter = painterResource(R.drawable.ic_symbol_close),
|
text = photos.getOrNull(pagerState.currentPage)?.name.orEmpty(),
|
||||||
contentDescription = stringResource(R.string.action_close),
|
color = Color.White,
|
||||||
tint = Color.White,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_symbol_close),
|
||||||
|
contentDescription = stringResource(R.string.action_close),
|
||||||
|
tint = Color.White,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -646,7 +649,6 @@ private fun ZoomablePhoto(
|
|||||||
animationSpec = tween(durationMillis = fadeMs),
|
animationSpec = tween(durationMillis = fadeMs),
|
||||||
label = "fullAlpha",
|
label = "fullAlpha",
|
||||||
)
|
)
|
||||||
val scale by remember { derivedStateOf { zoomState.zoomable.transform.scaleX } }
|
|
||||||
|
|
||||||
LaunchedEffect(photo.uri) {
|
LaunchedEffect(photo.uri) {
|
||||||
|
|
||||||
@@ -663,11 +665,13 @@ private fun ZoomablePhoto(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(scale) {
|
LaunchedEffect(zoomState) {
|
||||||
val zoomed = scale > 1.01f
|
snapshotFlow { zoomState.zoomable.transform.scaleX > 1.01f }
|
||||||
|
.distinctUntilChanged()
|
||||||
onZoomChange(zoomed)
|
.collect { zoomed ->
|
||||||
zoomState.subsampling.setDisabled(!zoomed)
|
onZoomChange(zoomed)
|
||||||
|
zoomState.subsampling.setDisabled(!zoomed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import coil3.SingletonImageLoader
|
import coil3.SingletonImageLoader
|
||||||
@@ -45,7 +46,6 @@ sealed interface GalleryDialog {
|
|||||||
data class DeleteSubfolder(val folder: FolderNode) : GalleryDialog
|
data class DeleteSubfolder(val folder: FolderNode) : GalleryDialog
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class MovePhotoSheetState(
|
data class MovePhotoSheetState(
|
||||||
val file: Photo? = null,
|
val file: Photo? = null,
|
||||||
val files: List<Photo> = emptyList(),
|
val files: List<Photo> = emptyList(),
|
||||||
@@ -96,26 +96,25 @@ data class GalleryUiState(
|
|||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
) {
|
) {
|
||||||
val isSelectionMode: Boolean = selectedPhotoUris.isNotEmpty()
|
val isSelectionMode: Boolean = selectedPhotoUris.isNotEmpty()
|
||||||
val selectedPhotos: List<Photo> = photos.filter { it.uri.toString() in selectedPhotoUris }
|
|
||||||
val sortedSubfolders: List<FolderNode> = if (sortAscending) {
|
val selectedPhotos: List<Photo> by lazy {
|
||||||
subfolders.sortedBy { it.name.lowercase() }
|
photos.filter { it.uri.toString() in selectedPhotoUris }
|
||||||
} else {
|
|
||||||
subfolders.sortedByDescending { it.name.lowercase() }
|
|
||||||
}
|
}
|
||||||
val sortedPhotos: List<Photo> = if (sortAscending) {
|
|
||||||
photos.sortedBy { it.name.lowercase() }
|
val selectablePhotoUris: Set<String> by lazy {
|
||||||
} else {
|
photos.asSequence()
|
||||||
photos.sortedByDescending { it.name.lowercase() }
|
.filter { it.kind == GalleryFileKind.Photo }
|
||||||
|
.mapTo(HashSet()) { it.uri.toString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val allPhotosSelected: Boolean by lazy {
|
||||||
|
selectablePhotoUris.isNotEmpty() && selectedPhotoUris.containsAll(selectablePhotoUris)
|
||||||
}
|
}
|
||||||
val selectablePhotoUris: Set<String> = photos
|
|
||||||
.filter { it.kind == GalleryFileKind.Photo }
|
|
||||||
.map { it.uri.toString() }
|
|
||||||
.toSet()
|
|
||||||
val allPhotosSelected: Boolean = selectablePhotoUris.isNotEmpty() && selectedPhotoUris.containsAll(selectablePhotoUris)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class GalleryViewModel @Inject constructor(
|
class GalleryViewModel @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
private val preferencesRepository: UserPreferencesRepository,
|
private val preferencesRepository: UserPreferencesRepository,
|
||||||
private val listGalleryUseCase: ListGalleryUseCase,
|
private val listGalleryUseCase: ListGalleryUseCase,
|
||||||
private val listFolderTreeUseCase: ListFolderTreeUseCase,
|
private val listFolderTreeUseCase: ListFolderTreeUseCase,
|
||||||
@@ -131,7 +130,11 @@ class GalleryViewModel @Inject constructor(
|
|||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState: StateFlow<GalleryUiState>
|
val uiState: StateFlow<GalleryUiState>
|
||||||
field: MutableStateFlow<GalleryUiState> = MutableStateFlow(GalleryUiState())
|
field: MutableStateFlow<GalleryUiState> = MutableStateFlow(
|
||||||
|
// Navigation puts type-safe route args into the SavedStateHandle, so the
|
||||||
|
// title is correct from the very first frame, before any listing completes.
|
||||||
|
GalleryUiState(folderName = savedStateHandle.get<String>("folderName").orEmpty()),
|
||||||
|
)
|
||||||
|
|
||||||
private var forceReload = false
|
private var forceReload = false
|
||||||
|
|
||||||
@@ -202,6 +205,9 @@ class GalleryViewModel @Inject constructor(
|
|||||||
loadJob = viewModelScope.launch {
|
loadJob = viewModelScope.launch {
|
||||||
uiState.value = uiState.value.copy(
|
uiState.value = uiState.value.copy(
|
||||||
currentFolderUri = folderUriString,
|
currentFolderUri = folderUriString,
|
||||||
|
folderName = state.folderName.ifEmpty {
|
||||||
|
folderUriString.substringAfterLast(':').substringAfterLast('/')
|
||||||
|
},
|
||||||
isLoading = true,
|
isLoading = true,
|
||||||
error = null,
|
error = null,
|
||||||
)
|
)
|
||||||
@@ -281,6 +287,10 @@ class GalleryViewModel @Inject constructor(
|
|||||||
statsLoaded = true,
|
statsLoaded = true,
|
||||||
)
|
)
|
||||||
uiState.value = uiState.value.copy(subfolders = updated)
|
uiState.value = uiState.value.copy(subfolders = updated)
|
||||||
|
} else if (!old.statsLoaded) {
|
||||||
|
val updated = current.toMutableList()
|
||||||
|
updated[idx] = old.copy(statsLoaded = true)
|
||||||
|
uiState.value = uiState.value.copy(subfolders = updated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prefetchThumbnailUris(stats.previews)
|
prefetchThumbnailUris(stats.previews)
|
||||||
@@ -343,18 +353,31 @@ class GalleryViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun clearSelection() {
|
fun clearSelection() {
|
||||||
dragSelectionBaseline = emptySet()
|
dragSelectionBaseline = emptySet()
|
||||||
|
dragSelectableCache = null
|
||||||
uiState.value = uiState.value.copy(selectedPhotoUris = emptySet())
|
uiState.value = uiState.value.copy(selectedPhotoUris = emptySet())
|
||||||
}
|
}
|
||||||
|
|
||||||
private var dragSelectionBaseline: Set<String> = emptySet()
|
private var dragSelectionBaseline: Set<String> = emptySet()
|
||||||
|
|
||||||
|
private var dragSelectableCache: Pair<List<Photo>, Set<String>>? = null
|
||||||
|
|
||||||
|
private fun currentSelectablePhotoUris(): Set<String> {
|
||||||
|
val photos = uiState.value.photos
|
||||||
|
dragSelectableCache?.let { (cachedPhotos, uris) ->
|
||||||
|
if (cachedPhotos === photos) return uris
|
||||||
|
}
|
||||||
|
val uris = uiState.value.selectablePhotoUris
|
||||||
|
dragSelectableCache = photos to uris
|
||||||
|
return uris
|
||||||
|
}
|
||||||
|
|
||||||
fun beginDragSelection(anchorUri: String) {
|
fun beginDragSelection(anchorUri: String) {
|
||||||
dragSelectionBaseline = uiState.value.selectedPhotoUris + anchorUri
|
dragSelectionBaseline = uiState.value.selectedPhotoUris + anchorUri
|
||||||
uiState.value = uiState.value.copy(selectedPhotoUris = dragSelectionBaseline)
|
uiState.value = uiState.value.copy(selectedPhotoUris = dragSelectionBaseline)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateDragSelection(rangeUris: Set<String>) {
|
fun updateDragSelection(rangeUris: Set<String>) {
|
||||||
val selectable = uiState.value.selectablePhotoUris
|
val selectable = currentSelectablePhotoUris()
|
||||||
uiState.value = uiState.value.copy(
|
uiState.value = uiState.value.copy(
|
||||||
selectedPhotoUris = dragSelectionBaseline + rangeUris.intersect(selectable),
|
selectedPhotoUris = dragSelectionBaseline + rangeUris.intersect(selectable),
|
||||||
)
|
)
|
||||||
@@ -362,6 +385,7 @@ class GalleryViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun endDragSelection() {
|
fun endDragSelection() {
|
||||||
dragSelectionBaseline = emptySet()
|
dragSelectionBaseline = emptySet()
|
||||||
|
dragSelectableCache = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun zipSelectedPhotosAndShare() {
|
fun zipSelectedPhotosAndShare() {
|
||||||
|
|||||||
@@ -4,20 +4,7 @@
|
|||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
|
|
||||||
<path
|
<path
|
||||||
android:strokeColor="#000000"
|
android:fillColor="#C9584D"
|
||||||
android:strokeWidth="3.375"
|
android:pathData="M66.90,53.85C66.90,54.93 66.82,56.08 66.67,57.08C66.53,58.07 66.30,58.95 66.00,59.80C65.71,60.66 65.34,61.46 64.91,62.20C64.48,62.94 63.97,63.63 63.41,64.25C62.86,64.87 62.23,65.42 61.56,65.90C60.89,66.38 60.16,66.79 59.39,67.11C58.61,67.44 57.82,67.69 56.92,67.85C56.02,68.02 54.97,68.10 54.00,68.10C53.03,68.10 51.98,68.02 51.08,67.85C50.18,67.69 49.39,67.44 48.61,67.11C47.84,66.79 47.11,66.38 46.44,65.90C45.77,65.42 45.14,64.87 44.59,64.25C44.03,63.63 43.52,62.94 43.09,62.20C42.66,61.46 42.29,60.66 42.00,59.80C41.70,58.95 41.47,58.07 41.33,57.08C41.18,56.08 41.10,54.93 41.10,53.85C41.10,52.77 41.18,51.62 41.33,50.62C41.47,49.63 41.70,48.75 42.00,47.90C42.29,47.04 42.66,46.24 43.09,45.50C43.52,44.76 44.03,44.07 44.59,43.45C45.14,42.83 45.77,42.28 46.44,41.80C47.11,41.32 47.84,40.91 48.61,40.59C49.39,40.26 50.18,40.01 51.08,39.85C51.98,39.68 53.03,39.60 54.00,39.60C54.97,39.60 56.02,39.68 56.92,39.85C57.82,40.01 58.61,40.26 59.39,40.59C60.16,40.91 60.89,41.32 61.56,41.80C62.23,42.28 62.86,42.83 63.41,43.45C63.97,44.07 64.48,44.76 64.91,45.50C65.34,46.24 65.71,47.04 66.00,47.90C66.30,48.75 66.53,49.63 66.67,50.62C66.82,51.62 66.90,52.77 66.90,53.85ZM61.50,54.00C61.50,53.30 61.46,52.54 61.37,51.90C61.29,51.25 61.16,50.67 60.99,50.12C60.82,49.56 60.61,49.03 60.36,48.55C60.12,48.07 59.83,47.62 59.51,47.21C59.20,46.81 58.84,46.45 58.46,46.14C58.08,45.83 57.66,45.56 57.22,45.35C56.78,45.13 56.33,44.97 55.81,44.86C55.30,44.75 54.70,44.70 54.15,44.70C53.60,44.70 53.00,44.75 52.49,44.86C51.97,44.97 51.52,45.13 51.08,45.35C50.64,45.56 50.22,45.83 49.84,46.14C49.46,46.45 49.10,46.81 48.79,47.21C48.47,47.62 48.18,48.07 47.94,48.55C47.69,49.03 47.48,49.56 47.31,50.12C47.14,50.67 47.01,51.25 46.93,51.90C46.84,52.54 46.80,53.30 46.80,54.00C46.80,54.70 46.84,55.46 46.93,56.10C47.01,56.75 47.14,57.33 47.31,57.88C47.48,58.44 47.69,58.97 47.94,59.45C48.18,59.93 48.47,60.38 48.79,60.79C49.10,61.19 49.46,61.55 49.84,61.86C50.22,62.17 50.64,62.44 51.08,62.65C51.52,62.87 51.97,63.03 52.49,63.14C53.00,63.25 53.60,63.30 54.15,63.30C54.70,63.30 55.30,63.25 55.81,63.14C56.33,63.03 56.78,62.87 57.22,62.65C57.66,62.44 58.08,62.17 58.46,61.86C58.84,61.55 59.20,61.19 59.51,60.79C59.83,60.38 60.12,59.93 60.36,59.45C60.61,58.97 60.82,58.44 60.99,57.88C61.16,57.33 61.29,56.75 61.37,56.10C61.46,55.46 61.50,54.70 61.50,54.00Z" />
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:pathData="M 43.453125 32.90625 L 64.546875 32.90625 a 10.546875 10.546875 0 0 1 10.546875 10.546875 L 75.09375 64.546875 a 10.546875 10.546875 0 0 1 -10.546875 10.546875 L 43.453125 75.09375 a 10.546875 10.546875 0 0 1 -10.546875 -10.546875 L 32.90625 43.453125 a 10.546875 10.546875 0 0 1 10.546875 -10.546875 Z" />
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:strokeColor="#000000"
|
|
||||||
android:strokeWidth="3.375"
|
|
||||||
android:pathData="M 41.34375 54 a 12.65625 12.65625 0 1 0 25.3125 0 a 12.65625 12.65625 0 1 0 -25.3125 0 Z" />
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="#d94e47"
|
|
||||||
android:pathData="M 68.71875 38.390625 a 2.53125 2.53125 0 1 0 5.0625 0 a 2.53125 2.53125 0 1 0 -5.0625 0 Z" />
|
|
||||||
|
|
||||||
</vector>
|
</vector>
|
||||||
@@ -4,20 +4,7 @@
|
|||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
|
|
||||||
<path
|
|
||||||
android:strokeColor="#FFFFFF"
|
|
||||||
android:strokeWidth="3.375"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:pathData="M 43.453125 32.90625 L 64.546875 32.90625 a 10.546875 10.546875 0 0 1 10.546875 10.546875 L 75.09375 64.546875 a 10.546875 10.546875 0 0 1 -10.546875 10.546875 L 43.453125 75.09375 a 10.546875 10.546875 0 0 1 -10.546875 -10.546875 L 32.90625 43.453125 a 10.546875 10.546875 0 0 1 10.546875 -10.546875 Z" />
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:strokeColor="#FFFFFF"
|
|
||||||
android:strokeWidth="3.375"
|
|
||||||
android:pathData="M 41.34375 54 a 12.65625 12.65625 0 1 0 25.3125 0 a 12.65625 12.65625 0 1 0 -25.3125 0 Z" />
|
|
||||||
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M 67.5 38.390625 a 2.109375 2.109375 0 1 0 4.21875 0 a 2.109375 2.109375 0 1 0 -4.21875 0 Z" />
|
android:pathData="M66.90,53.85C66.90,54.93 66.82,56.08 66.67,57.08C66.53,58.07 66.30,58.95 66.00,59.80C65.71,60.66 65.34,61.46 64.91,62.20C64.48,62.94 63.97,63.63 63.41,64.25C62.86,64.87 62.23,65.42 61.56,65.90C60.89,66.38 60.16,66.79 59.39,67.11C58.61,67.44 57.82,67.69 56.92,67.85C56.02,68.02 54.97,68.10 54.00,68.10C53.03,68.10 51.98,68.02 51.08,67.85C50.18,67.69 49.39,67.44 48.61,67.11C47.84,66.79 47.11,66.38 46.44,65.90C45.77,65.42 45.14,64.87 44.59,64.25C44.03,63.63 43.52,62.94 43.09,62.20C42.66,61.46 42.29,60.66 42.00,59.80C41.70,58.95 41.47,58.07 41.33,57.08C41.18,56.08 41.10,54.93 41.10,53.85C41.10,52.77 41.18,51.62 41.33,50.62C41.47,49.63 41.70,48.75 42.00,47.90C42.29,47.04 42.66,46.24 43.09,45.50C43.52,44.76 44.03,44.07 44.59,43.45C45.14,42.83 45.77,42.28 46.44,41.80C47.11,41.32 47.84,40.91 48.61,40.59C49.39,40.26 50.18,40.01 51.08,39.85C51.98,39.68 53.03,39.60 54.00,39.60C54.97,39.60 56.02,39.68 56.92,39.85C57.82,40.01 58.61,40.26 59.39,40.59C60.16,40.91 60.89,41.32 61.56,41.80C62.23,42.28 62.86,42.83 63.41,43.45C63.97,44.07 64.48,44.76 64.91,45.50C65.34,46.24 65.71,47.04 66.00,47.90C66.30,48.75 66.53,49.63 66.67,50.62C66.82,51.62 66.90,52.77 66.90,53.85ZM61.50,54.00C61.50,53.30 61.46,52.54 61.37,51.90C61.29,51.25 61.16,50.67 60.99,50.12C60.82,49.56 60.61,49.03 60.36,48.55C60.12,48.07 59.83,47.62 59.51,47.21C59.20,46.81 58.84,46.45 58.46,46.14C58.08,45.83 57.66,45.56 57.22,45.35C56.78,45.13 56.33,44.97 55.81,44.86C55.30,44.75 54.70,44.70 54.15,44.70C53.60,44.70 53.00,44.75 52.49,44.86C51.97,44.97 51.52,45.13 51.08,45.35C50.64,45.56 50.22,45.83 49.84,46.14C49.46,46.45 49.10,46.81 48.79,47.21C48.47,47.62 48.18,48.07 47.94,48.55C47.69,49.03 47.48,49.56 47.31,50.12C47.14,50.67 47.01,51.25 46.93,51.90C46.84,52.54 46.80,53.30 46.80,54.00C46.80,54.70 46.84,55.46 46.93,56.10C47.01,56.75 47.14,57.33 47.31,57.88C47.48,58.44 47.69,58.97 47.94,59.45C48.18,59.93 48.47,60.38 48.79,60.79C49.10,61.19 49.46,61.55 49.84,61.86C50.22,62.17 50.64,62.44 51.08,62.65C51.52,62.87 51.97,63.03 52.49,63.14C53.00,63.25 53.60,63.30 54.15,63.30C54.70,63.30 55.30,63.25 55.81,63.14C56.33,63.03 56.78,62.87 57.22,62.65C57.66,62.44 58.08,62.17 58.46,61.86C58.84,61.55 59.20,61.19 59.51,60.79C59.83,60.38 60.12,59.93 60.36,59.45C60.61,58.97 60.82,58.44 60.99,57.88C61.16,57.33 61.29,56.75 61.37,56.10C61.46,55.46 61.50,54.70 61.50,54.00Z" />
|
||||||
|
|
||||||
</vector>
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -33,7 +33,7 @@
|
|||||||
<string name="folders_root_body">Select a root folder. OneCamera will organize photos into folders inside it.</string>
|
<string name="folders_root_body">Select a root folder. OneCamera will organize photos into folders inside it.</string>
|
||||||
<string name="folders_choose_root">Choose folder</string>
|
<string name="folders_choose_root">Choose folder</string>
|
||||||
<string name="folders_empty_title">No folders yet</string>
|
<string name="folders_empty_title">No folders yet</string>
|
||||||
<string name="folders_empty_body">Create a folder to start organizing your photos.</string>
|
<string name="folders_empty_body">Create a folder to start organizing photos</string>
|
||||||
<string name="folders_empty_action">Create first folder</string>
|
<string name="folders_empty_action">Create first folder</string>
|
||||||
<string name="folders_share_chooser">Share %1$s</string>
|
<string name="folders_share_chooser">Share %1$s</string>
|
||||||
<string name="folders_share_no_app">No app is available to share this archive.</string>
|
<string name="folders_share_no_app">No app is available to share this archive.</string>
|
||||||
@@ -46,7 +46,6 @@
|
|||||||
<item quantity="other">%d photos</item>
|
<item quantity="other">%d photos</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="gallery_title">Gallery</string>
|
|
||||||
<string name="gallery_create_subfolder">Create subfolder</string>
|
<string name="gallery_create_subfolder">Create subfolder</string>
|
||||||
<string name="gallery_sort_ascending">Sort ascending</string>
|
<string name="gallery_sort_ascending">Sort ascending</string>
|
||||||
<string name="gallery_sort_descending">Sort descending</string>
|
<string name="gallery_sort_descending">Sort descending</string>
|
||||||
@@ -56,7 +55,7 @@
|
|||||||
<string name="gallery_delete_photo_title">Delete photo?</string>
|
<string name="gallery_delete_photo_title">Delete photo?</string>
|
||||||
<string name="gallery_delete_photo_message">Delete “%1$s”? This action cannot be undone.</string>
|
<string name="gallery_delete_photo_message">Delete “%1$s”? This action cannot be undone.</string>
|
||||||
<string name="gallery_empty_title">No photos yet</string>
|
<string name="gallery_empty_title">No photos yet</string>
|
||||||
<string name="gallery_empty_body">Use the camera button to add your first photo.</string>
|
<string name="gallery_empty_body">Use the camera button to add the first photo</string>
|
||||||
<string name="gallery_photo">Photo</string>
|
<string name="gallery_photo">Photo</string>
|
||||||
<string name="gallery_zip_archive">ZIP archive</string>
|
<string name="gallery_zip_archive">ZIP archive</string>
|
||||||
<string name="gallery_share_photos_chooser">Share photos</string>
|
<string name="gallery_share_photos_chooser">Share photos</string>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ plugins {
|
|||||||
alias(libs.plugins.ksp) apply false
|
alias(libs.plugins.ksp) apply false
|
||||||
alias(libs.plugins.kotlin.serialization) apply false
|
alias(libs.plugins.kotlin.serialization) apply false
|
||||||
alias(libs.plugins.baselineprofile) apply false
|
alias(libs.plugins.baselineprofile) apply false
|
||||||
id("com.github.ben-manes.versions") version "0.64.0"
|
id("com.github.ben-manes.versions") version "0.66.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("assembleDebug") {
|
tasks.register("assembleDebug") {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ datastorePreferences = "1.2.1"
|
|||||||
documentfile = "1.1.0"
|
documentfile = "1.1.0"
|
||||||
coil = "3.6.3"
|
coil = "3.6.3"
|
||||||
zoomimage = "1.7.0"
|
zoomimage = "1.7.0"
|
||||||
kotlinxSerializationJson = "1.11.0"
|
kotlinxSerializationJson = "1.12.0"
|
||||||
benchmark = "1.5.0"
|
benchmark = "1.5.0"
|
||||||
profileinstaller = "1.4.1"
|
profileinstaller = "1.4.1"
|
||||||
tracing = "2.0.4"
|
tracing = "2.0.4"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
retries=0
|
retries=0
|
||||||
retryBackOffMs=500
|
retryBackOffMs=500
|
||||||
|
|||||||