Compare commits
2
Commits
9910ba6917
...
76ea8ba9a0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76ea8ba9a0 | ||
|
|
c2ddfdf89d |
No files matched your search
@@ -37,7 +37,9 @@ import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import com.example.onecamera.R
|
||||
import com.example.onecamera.ui.components.OneCameraEmptyState
|
||||
import com.example.onecamera.ui.components.OneCameraScreenPadding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
@@ -111,6 +113,7 @@ fun SystemCameraCapture(
|
||||
LaunchedEffect(hasLaunchedCamera) {
|
||||
if (!hasLaunchedCamera) {
|
||||
viewModel.clearLastSaveResult()
|
||||
val (preparedPath, preparedUri) = withContext(Dispatchers.IO) {
|
||||
val tempDir = File(context.cacheDir, "camera_captures").apply { mkdirs() }
|
||||
|
||||
tempDir.listFiles()?.forEach { runCatching { it.delete() } }
|
||||
@@ -122,12 +125,14 @@ fun SystemCameraCapture(
|
||||
"${context.packageName}.fileprovider",
|
||||
tempFile,
|
||||
)
|
||||
tempPhotoUriString = uri.toString()
|
||||
tempPhotoPath = tempFile.absolutePath
|
||||
tempFile.absolutePath to uri.toString()
|
||||
}
|
||||
tempPhotoUriString = preparedUri
|
||||
tempPhotoPath = preparedPath
|
||||
hasLaunchedCamera = true
|
||||
hasReceivedCameraResult = false
|
||||
launchUptimeMillis = SystemClock.elapsedRealtime()
|
||||
takePictureLauncher.launch(uri)
|
||||
takePictureLauncher.launch(Uri.parse(preparedUri))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +148,7 @@ fun SystemCameraCapture(
|
||||
tempPhotoUriString != null
|
||||
) {
|
||||
val tempLength = tempPhotoPath?.let { path ->
|
||||
runCatching { File(path).length() }.getOrNull()
|
||||
runCatching { withContext(Dispatchers.IO) { File(path).length() } }.getOrNull()
|
||||
} ?: 0L
|
||||
val tempPhotoUri = tempPhotoUriString?.let(Uri::parse)
|
||||
if (tempLength > 0L && tempPhotoUri != null) {
|
||||
|
||||
@@ -105,7 +105,11 @@ fun FolderTreePickerSheet(
|
||||
.fillMaxWidth()
|
||||
.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 isSelected = selectedFolderUri == folder.uri.toString()
|
||||
ListItem(
|
||||
|
||||
@@ -41,16 +41,16 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -86,6 +86,7 @@ import com.example.onecamera.ui.folders.RenameFolderDialog
|
||||
import com.github.panpf.zoomimage.CoilZoomAsyncImage
|
||||
import com.github.panpf.zoomimage.rememberCoilZoomState
|
||||
import com.github.panpf.zoomimage.zoom.GestureType
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
@@ -198,11 +199,9 @@ fun GalleryScreen(
|
||||
uiState.subfolders.sortedByDescending { it.name.lowercase() }
|
||||
}
|
||||
}
|
||||
val sortedGalleryPhotos by remember(sortedPhotos) {
|
||||
derivedStateOf {
|
||||
val sortedGalleryPhotos = remember(sortedPhotos) {
|
||||
sortedPhotos.filter { it.kind == GalleryFileKind.Photo }
|
||||
}
|
||||
}
|
||||
|
||||
when (val dialog = uiState.dialog) {
|
||||
GalleryDialog.CreateSubfolder -> {
|
||||
@@ -537,11 +536,9 @@ internal fun FullscreenPhotoPagerContent(
|
||||
onDismiss: () -> Unit,
|
||||
onDelete: (Photo) -> Unit,
|
||||
) {
|
||||
val initialPage by remember(photos, initialPhoto.uri) {
|
||||
derivedStateOf {
|
||||
val initialPage = remember(photos, initialPhoto.uri) {
|
||||
photos.indexOfFirst { it.uri == initialPhoto.uri }.coerceAtLeast(0)
|
||||
}
|
||||
}
|
||||
val pagerState = rememberPagerState(initialPage = initialPage) { photos.size.coerceAtLeast(1) }
|
||||
|
||||
var isZoomed by remember { mutableStateOf(false) }
|
||||
@@ -646,7 +643,6 @@ private fun ZoomablePhoto(
|
||||
animationSpec = tween(durationMillis = fadeMs),
|
||||
label = "fullAlpha",
|
||||
)
|
||||
val scale by remember { derivedStateOf { zoomState.zoomable.transform.scaleX } }
|
||||
|
||||
LaunchedEffect(photo.uri) {
|
||||
|
||||
@@ -663,25 +659,27 @@ private fun ZoomablePhoto(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(scale) {
|
||||
val zoomed = scale > 1.01f
|
||||
|
||||
LaunchedEffect(zoomState) {
|
||||
snapshotFlow { zoomState.zoomable.transform.scaleX > 1.01f }
|
||||
.distinctUntilChanged()
|
||||
.collect { zoomed ->
|
||||
onZoomChange(zoomed)
|
||||
zoomState.subsampling.setDisabled(!zoomed)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
|
||||
if (fullAlpha < 1f) {
|
||||
AsyncImage(
|
||||
model = remember(photo.uri, context, fadeMs) {
|
||||
thumbnailDraftRequest(context, photo.uri, crossfadeMillis = fadeMs)
|
||||
},
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer { alpha = 1f - fullAlpha },
|
||||
)
|
||||
}
|
||||
CoilZoomAsyncImage(
|
||||
model = fullImageRequest,
|
||||
contentDescription = photo.name,
|
||||
@@ -690,7 +688,7 @@ private fun ZoomablePhoto(
|
||||
onError = { isFullReady = false },
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(fullAlpha),
|
||||
.graphicsLayer { alpha = fullAlpha },
|
||||
zoomState = zoomState,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ sealed interface GalleryDialog {
|
||||
data class DeleteSubfolder(val folder: FolderNode) : GalleryDialog
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class MovePhotoSheetState(
|
||||
val file: Photo? = null,
|
||||
val files: List<Photo> = emptyList(),
|
||||
@@ -96,22 +95,20 @@ data class GalleryUiState(
|
||||
val error: String? = null,
|
||||
) {
|
||||
val isSelectionMode: Boolean = selectedPhotoUris.isNotEmpty()
|
||||
val selectedPhotos: List<Photo> = photos.filter { it.uri.toString() in selectedPhotoUris }
|
||||
val sortedSubfolders: List<FolderNode> = if (sortAscending) {
|
||||
subfolders.sortedBy { it.name.lowercase() }
|
||||
} else {
|
||||
subfolders.sortedByDescending { it.name.lowercase() }
|
||||
|
||||
val selectedPhotos: List<Photo> by lazy {
|
||||
photos.filter { it.uri.toString() in selectedPhotoUris }
|
||||
}
|
||||
val sortedPhotos: List<Photo> = if (sortAscending) {
|
||||
photos.sortedBy { it.name.lowercase() }
|
||||
} else {
|
||||
photos.sortedByDescending { it.name.lowercase() }
|
||||
}
|
||||
val selectablePhotoUris: Set<String> = photos
|
||||
|
||||
val selectablePhotoUris: Set<String> by lazy {
|
||||
photos.asSequence()
|
||||
.filter { it.kind == GalleryFileKind.Photo }
|
||||
.map { it.uri.toString() }
|
||||
.toSet()
|
||||
val allPhotosSelected: Boolean = selectablePhotoUris.isNotEmpty() && selectedPhotoUris.containsAll(selectablePhotoUris)
|
||||
.mapTo(HashSet()) { it.uri.toString() }
|
||||
}
|
||||
|
||||
val allPhotosSelected: Boolean by lazy {
|
||||
selectablePhotoUris.isNotEmpty() && selectedPhotoUris.containsAll(selectablePhotoUris)
|
||||
}
|
||||
}
|
||||
|
||||
@HiltViewModel
|
||||
@@ -343,18 +340,31 @@ class GalleryViewModel @Inject constructor(
|
||||
|
||||
fun clearSelection() {
|
||||
dragSelectionBaseline = emptySet()
|
||||
dragSelectableCache = null
|
||||
uiState.value = uiState.value.copy(selectedPhotoUris = 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) {
|
||||
dragSelectionBaseline = uiState.value.selectedPhotoUris + anchorUri
|
||||
uiState.value = uiState.value.copy(selectedPhotoUris = dragSelectionBaseline)
|
||||
}
|
||||
|
||||
fun updateDragSelection(rangeUris: Set<String>) {
|
||||
val selectable = uiState.value.selectablePhotoUris
|
||||
val selectable = currentSelectablePhotoUris()
|
||||
uiState.value = uiState.value.copy(
|
||||
selectedPhotoUris = dragSelectionBaseline + rangeUris.intersect(selectable),
|
||||
)
|
||||
@@ -362,6 +372,7 @@ class GalleryViewModel @Inject constructor(
|
||||
|
||||
fun endDragSelection() {
|
||||
dragSelectionBaseline = emptySet()
|
||||
dragSelectableCache = null
|
||||
}
|
||||
|
||||
fun zipSelectedPhotosAndShare() {
|
||||
|
||||
Reference in new issue
Block a user