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.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) {
|
||||||
|
|||||||
@@ -105,7 +105,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(
|
||||||
|
|||||||
@@ -41,16 +41,16 @@ 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
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
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.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
|
||||||
@@ -198,10 +199,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) {
|
||||||
@@ -537,10 +536,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) }
|
||||||
|
|
||||||
@@ -646,7 +643,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,25 +659,27 @@ 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()) {
|
||||||
|
|
||||||
if (fullAlpha < 1f) {
|
AsyncImage(
|
||||||
AsyncImage(
|
model = remember(photo.uri, context, fadeMs) {
|
||||||
model = remember(photo.uri, context, fadeMs) {
|
thumbnailDraftRequest(context, photo.uri, crossfadeMillis = fadeMs)
|
||||||
thumbnailDraftRequest(context, photo.uri, crossfadeMillis = fadeMs)
|
},
|
||||||
},
|
contentDescription = null,
|
||||||
contentDescription = null,
|
contentScale = ContentScale.Fit,
|
||||||
contentScale = ContentScale.Fit,
|
modifier = Modifier
|
||||||
modifier = Modifier.fillMaxSize(),
|
.fillMaxSize()
|
||||||
)
|
.graphicsLayer { alpha = 1f - fullAlpha },
|
||||||
}
|
)
|
||||||
CoilZoomAsyncImage(
|
CoilZoomAsyncImage(
|
||||||
model = fullImageRequest,
|
model = fullImageRequest,
|
||||||
contentDescription = photo.name,
|
contentDescription = photo.name,
|
||||||
@@ -690,7 +688,7 @@ private fun ZoomablePhoto(
|
|||||||
onError = { isFullReady = false },
|
onError = { isFullReady = false },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.alpha(fullAlpha),
|
.graphicsLayer { alpha = fullAlpha },
|
||||||
zoomState = zoomState,
|
zoomState = zoomState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,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,22 +95,20 @@ 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
|
||||||
@@ -343,18 +340,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 +372,7 @@ class GalleryViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun endDragSelection() {
|
fun endDragSelection() {
|
||||||
dragSelectionBaseline = emptySet()
|
dragSelectionBaseline = emptySet()
|
||||||
|
dragSelectableCache = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun zipSelectedPhotosAndShare() {
|
fun zipSelectedPhotosAndShare() {
|
||||||
|
|||||||
Reference in new issue
Block a user