From ab517f3af53ca6451c13f631318ecf7b211106da Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 20:04:24 +0000 Subject: [PATCH] refactor: move podcast show/trailer, geocache log and visualizer settings to commonsUI - BookmarkGroupItemOptions shares through rememberTextSharer instead of an ACTION_SEND intent. - DocumentSelectSingle (expect/actual) picks one document by MIME type; the trailer composer uses it, and the geocache log uses GallerySelectSingle(imagesOnly = true). - MediaUri.lastPathSegmentOrNull() replaces Uri.lastPathSegment in the composer view models. - rfc2822Date (commons, tested against java.time's RFC_1123_DATE_TIME) replaces java.time in PodcastComposerMedia; the MediaMetadataRetriever probe moves to the app-side PodcastAudioProbe. - The canvas-only FakeWaveformAnimation moves to commonsUI; the media3 variant stays in the app. - Moved (9 files): the audio visualizer settings, geocache find log, podcast show editor and podcast trailer screens with their view models. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../wavefront/FakeWaveformAnimation.kt | 74 +------------ .../amethyst/ui/navigation/AppNavigation.kt | 8 +- .../display/BookmarkGroupItemOptions.kt | 23 +--- .../loggedIn/music/NewMusicTrackViewModel.kt | 3 +- .../authoring/NewPodcastEpisodeScreen.kt | 1 + .../authoring/NewPodcastEpisodeViewModel.kt | 6 +- .../podcasts/authoring/PodcastAudioProbe.kt | 51 +++++++++ .../service/uploads/MediaUri.android.kt | 2 + .../amethyst/commons/podcasts/Rfc2822Date.kt | 52 +++++++++ .../commons/service/uploads/MediaUri.kt | 3 + .../commons/service/uploads/MediaUri.ios.kt | 8 ++ .../commons/service/uploads/MediaUri.jvm.kt | 8 ++ .../commons/podcasts/Rfc2822DateTest.kt | 44 ++++++++ .../ui/actions/uploads/FileSelect.android.kt | 15 +++ .../ui/actions/uploads/SelectFromFiles.kt | 7 ++ .../wavefront/FakeWaveformAnimation.kt | 100 ++++++++++++++++++ .../geocaches/log/LogGeocacheFindScreen.kt | 23 ++-- .../geocaches/log/LogGeocacheFindViewModel.kt | 6 +- .../authoring/EditPodcastShowScreen.kt | 6 +- .../authoring/EditPodcastShowViewModel.kt | 2 +- .../authoring/NewPodcastTrailerScreen.kt | 23 ++-- .../authoring/NewPodcastTrailerViewModel.kt | 7 +- .../authoring/PodcastComposerMedia.kt | 41 ++----- .../podcasts/authoring/V4VSplitEditor.kt | 2 +- .../settings/AudioVisualizerSettingsScreen.kt | 4 +- .../ui/actions/uploads/FileSelect.ios.kt | 9 ++ .../ui/actions/uploads/FileSelect.jvm.kt | 9 ++ 27 files changed, 363 insertions(+), 174 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastAudioProbe.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822Date.kt create mode 100644 commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822DateTest.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/wavefront/FakeWaveformAnimation.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt (92%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt (75%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/authoring/V4VSplitEditor.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/settings/AudioVisualizerSettingsScreen.kt (97%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/FakeWaveformAnimation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/FakeWaveformAnimation.kt index 1443c3ab45..f14983fde2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/FakeWaveformAnimation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/FakeWaveformAnimation.kt @@ -20,29 +20,15 @@ */ package com.vitorpamplona.amethyst.service.playback.composable.wavefront -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableFloatState import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.Fill -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.media3.common.Player -import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.commons.ui.components.wavefront.FakeWaveformAnimation import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -50,7 +36,6 @@ import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart -import kotlin.math.sin @Composable fun FakeWaveformAnimation( @@ -97,60 +82,3 @@ fun pollCurrentPosition(controller: Player) = emit(controller.currentPosition) }.flowOn(Dispatchers.IO) .conflate() - -@Preview -@Composable -fun FakeWaveformAnimationPreview() { - val state = - remember { - mutableFloatStateOf((500 % 1000.0f) / 1000.0f) - } - ThemeComparisonColumn { - FakeWaveformAnimation(state, 50, Modifier.width(200.dp)) - } -} - -@Composable -fun FakeWaveformAnimation( - waveformProgress: MutableFloatState, - barCount: Int = 50, - modifier: Modifier = Modifier, -) { - val barBrush = - Brush.linearGradient( - colors = listOf(Color(0xff2598cf), Color(0xff652d80)), - start = Offset(0.2f * 128f, 0f), - end = Offset(0.2f * 128f + 128f, 0f), - ) - - Canvas( - modifier = - modifier - .fillMaxWidth() - .requiredHeight(48.dp) - .padding(horizontal = 10.dp), - ) { - val barWidth = 3.dp.toPx() - val barPadding = 2.dp.toPx() - val barRadius = 2.dp.toPx() - val totalBarWidth = barWidth + barPadding - val startX = (size.width - (barCount * totalBarWidth - barPadding)) / 2f - - for (i in 0 until barCount) { - val phase = (i.toFloat() / barCount) * 2f * Math.PI.toFloat() - val wave = sin(waveformProgress.floatValue * 2f * Math.PI.toFloat() + phase) - val barFraction = ((wave + 1f) / 2f) - val barHeight = size.height * barFraction - val x = startX + i * totalBarWidth - val y = (size.height - barHeight) / 2f - - drawRoundRect( - brush = barBrush, - topLeft = Offset(x, y), - size = Size(barWidth, barHeight), - cornerRadius = CornerRadius(barRadius, barRadius), - style = Fill, - ) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 3ab4965e21..b7271fb601 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -121,6 +121,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.favorites.FavoriteA import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list.FollowPacksScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.hunt.GeocacheHuntScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.hunt.NewGeocacheHuntScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.log.LogGeocacheFindScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.GeoHashScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.gitRepositories.GitRepositoriesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.HashtagScreen @@ -143,6 +144,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nsites.NsitesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pinnednotes.PinnedNotesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.PodcastEpisodesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.PodcastsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.EditPodcastShowScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.NewPodcastTrailerScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.PodcastAuthoringScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.PollsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.results.PollResultsScreen @@ -158,6 +161,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.eventsync.Ev import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip43.RelayMembersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.vanish.VanishEventsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.AllSettingsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.AudioVisualizerSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.BlockedUsersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.BottomBarSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.CallSettingsScreen @@ -279,7 +283,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.FollowPack import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.GeocachesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.create.NewGeocacheScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.detail.GeocacheDetailScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.log.LogGeocacheFindScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitNewIssueScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryCodeScreen @@ -306,9 +309,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationS import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.PicturesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.PodcastScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.EditPodcastShowScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.NewPodcastEpisodeScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.NewPodcastTrailerScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.PollPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.SendPaymentScreen @@ -319,7 +320,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.subscriptions.Active import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.vanish.RequestToVanishScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.ScheduledPostsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AudioVisualizerSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ComposeSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt index b6b8eb9a21..5c658ad8ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.display -import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue @@ -29,7 +28,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note @@ -60,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeEditDraftTo @@ -171,7 +170,7 @@ fun BookmarkGroupItemOptionsMenu( onDismiss = onDismiss, ) { val clipboardManager = LocalClipboard.current - val actContext = LocalContext.current + val sharer = rememberTextSharer() val scope = rememberCoroutineScope() // Bookmark Management section @@ -240,23 +239,7 @@ fun BookmarkGroupItemOptionsMenu( } } M3ActionRow(icon = MaterialSymbols.Share, text = stringRes(Res.string.quick_action_share)) { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra( - Intent.EXTRA_TEXT, - externalLinkForNote(note), - ) - putExtra( - Intent.EXTRA_TITLE, - quickActionShareBrowserLinkStr, - ) - } - - val shareIntent = - Intent.createChooser(sendIntent, quickActionShareStr) - actContext.startActivity(shareIntent) + sharer.share(externalLinkForNote(note), quickActionShareBrowserLinkStr, quickActionShareStr) onDismiss() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt index 98dceec6da..b4332daaea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.service.uploads.lastPathSegmentOrNull import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -176,7 +177,7 @@ class NewMusicTrackViewModel : ViewModel() { audioMedia.value = MultiOrchestrator(persistentListOf(uri)) // Fall back to the last path segment of the content:// URI — Android picker URIs // rarely expose a clean filename, but the segment is at least stable across recomposes. - pickedAudioName.value = uri.uri.lastPathSegment?.substringAfterLast('/') + pickedAudioName.value = uri.uri.lastPathSegmentOrNull()?.substringAfterLast('/') // Auto-fill title/artist/album/duration from the picked file's embedded metadata so // the user doesn't have to retype what's already in the ID3 / mp4 / FLAC tags. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt index 80256104c8..d8126aaeff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt @@ -98,6 +98,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.CoverImagePicker import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.UploadInProgressBanner import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.UploadPlaceholder +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.V4VSplitEditor import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt index f0e20f62ae..5579d02896 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt @@ -33,6 +33,8 @@ import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.lastPathSegmentOrNull +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.PodcastComposerMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nipXXPodcasting20.episode.Podcasting20EpisodeEvent @@ -154,11 +156,11 @@ class NewPodcastEpisodeViewModel : ViewModel() { return } audioMedia.value = MultiOrchestrator(persistentListOf(uri)) - pickedAudioName.value = uri.uri.lastPathSegment?.substringAfterLast('/') + pickedAudioName.value = uri.uri.lastPathSegmentOrNull()?.substringAfterLast('/') val appContext = context.applicationContext viewModelScope.launch(Dispatchers.IO) { - val probed = PodcastComposerMedia.probeAudio(appContext, uri.uri) ?: return@launch + val probed = PodcastAudioProbe.probeAudio(appContext, uri.uri) ?: return@launch withContext(Dispatchers.Main.immediate) { probed.durationSeconds?.let { durationSeconds.value = it.toString() } if (title.value.isBlank()) probed.title?.let { title.value = it } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastAudioProbe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastAudioProbe.kt new file mode 100644 index 0000000000..2d1847a7c2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastAudioProbe.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring + +import android.content.Context +import android.media.MediaMetadataRetriever +import android.net.Uri +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.PodcastComposerMedia + +object PodcastAudioProbe { + /** + * Reads duration + title from a picked audio file via [MediaMetadataRetriever] (heavy — call off + * the main thread). Returns null if the provider rejects it or the file isn't a real container. + */ + fun probeAudio( + context: Context, + uri: Uri, + ): PodcastComposerMedia.ProbedAudio? { + val retriever = MediaMetadataRetriever() + return try { + retriever.setDataSource(context, uri) + val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() + PodcastComposerMedia.ProbedAudio( + durationSeconds = durationMs?.let { (it / 1000).toInt().takeIf { secs -> secs > 0 } }, + title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)?.trim()?.ifBlank { null }, + ) + } catch (_: Exception) { + null + } finally { + retriever.release() + } + } +} diff --git a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt index 5a7e949672..b38e335f3f 100644 --- a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt @@ -23,3 +23,5 @@ package com.vitorpamplona.amethyst.commons.service.uploads import android.net.Uri actual typealias MediaUri = Uri + +actual fun MediaUri.lastPathSegmentOrNull(): String? = lastPathSegment diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822Date.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822Date.kt new file mode 100644 index 0000000000..86ea1ca0b0 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822Date.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.podcasts + +private val WEEKDAYS = arrayOf("Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed") +private val MONTHS = arrayOf("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") + +/** + * [epochSeconds] as an RFC 2822 date in GMT (`Tue, 24 Jun 2025 12:00:00 GMT`), the form + * Podcasting 2.0's `pubdate` and RSS use. Matches `java.time`'s `RFC_1123_DATE_TIME`, which + * does not zero-pad the day. + */ +fun rfc2822Date(epochSeconds: Long): String { + val days = epochSeconds.floorDiv(86_400L) + val secondOfDay = epochSeconds.mod(86_400L) + + // Civil-from-days (Howard Hinnant), proleptic Gregorian. + val z = days + 719_468L + val era = z.floorDiv(146_097L) + val doe = z - era * 146_097L + val yoe = (doe - doe / 1_460L + doe / 36_524L - doe / 146_096L) / 365L + val doy = doe - (365L * yoe + yoe / 4L - yoe / 100L) + val mp = (5L * doy + 2L) / 153L + val day = doy - (153L * mp + 2L) / 5L + 1L + val month = if (mp < 10L) mp + 3L else mp - 9L + val year = yoe + era * 400L + if (month <= 2L) 1L else 0L + + val weekday = WEEKDAYS[days.mod(7L).toInt()] + val hh = (secondOfDay / 3_600L).toString().padStart(2, '0') + val mm = (secondOfDay / 60L % 60L).toString().padStart(2, '0') + val ss = (secondOfDay % 60L).toString().padStart(2, '0') + + return "$weekday, $day ${MONTHS[(month - 1L).toInt()]} $year $hh:$mm:$ss GMT" +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt index f7459cf988..daef1f6d3a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt @@ -29,3 +29,6 @@ expect abstract class MediaUri { /** The address as a string (`content://…` on Android). */ abstract override fun toString(): String } + +/** The last path segment of this address, if it has one (on Android, `Uri.lastPathSegment`). */ +expect fun MediaUri.lastPathSegmentOrNull(): String? diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt index eebaa3945a..e367e7b8f9 100644 --- a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt @@ -30,3 +30,11 @@ class StringMediaUri( ) : MediaUri() { override fun toString(): String = value } + +actual fun MediaUri.lastPathSegmentOrNull(): String? = + toString() + .substringBefore('?') + .substringBefore('#') + .trimEnd('/') + .substringAfterLast('/') + .ifBlank { null } diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt index eebaa3945a..e367e7b8f9 100644 --- a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt @@ -30,3 +30,11 @@ class StringMediaUri( ) : MediaUri() { override fun toString(): String = value } + +actual fun MediaUri.lastPathSegmentOrNull(): String? = + toString() + .substringBefore('?') + .substringBefore('#') + .trimEnd('/') + .substringAfterLast('/') + .ifBlank { null } diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822DateTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822DateTest.kt new file mode 100644 index 0000000000..8e1eff305a --- /dev/null +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822DateTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.podcasts + +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import kotlin.test.Test +import kotlin.test.assertEquals + +class Rfc2822DateTest { + private fun javaTime(epochSeconds: Long) = DateTimeFormatter.RFC_1123_DATE_TIME.format(Instant.ofEpochSecond(epochSeconds).atZone(ZoneId.of("GMT"))) + + @Test + fun knownDate() { + assertEquals("Tue, 24 Jun 2025 12:00:00 GMT", rfc2822Date(1_750_766_400L)) + } + + @Test + fun matchesJavaTimeAcrossDates() { + val samples = + listOf(0L, 59L, 86_399L, 86_400L, 951_782_400L, 951_868_799L, 1_709_164_800L, 4_107_542_399L, 1_750_766_400L) + + (0 until 2_000).map { it * 2_629_743L + it * 7_919L } + samples.forEach { assertEquals(javaTime(it), rfc2822Date(it), "epoch $it") } + } +} diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.android.kt index 64f47649c6..dccb258f2a 100644 --- a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.android.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.android.kt @@ -24,6 +24,7 @@ import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -69,3 +70,17 @@ actual fun FileSelect(onFilesSelected: (ImmutableList) -> Unit) { LaunchFilePicker() } + +@Composable +actual fun DocumentSelectSingle( + mimeTypes: List, + onPicked: (SelectedMedia?) -> Unit, +) { + val resolver = LocalContext.current.contentResolver + val launcher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument(), + onResult = { uri: Uri? -> onPicked(uri?.let { SelectedMedia(it, resolver.getType(it)) }) }, + ) + LaunchedEffect(Unit) { launcher.launch(mimeTypes.toTypedArray()) } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromFiles.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromFiles.kt index 652b4963c9..c4458c2768 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromFiles.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromFiles.kt @@ -91,3 +91,10 @@ private fun FileSelectButton( /** Opens the platform's document picker for audio and PDF files; reports an empty list on cancel. */ @Composable expect fun FileSelect(onFilesSelected: (ImmutableList) -> Unit = {}) + +/** Opens the platform's document picker for one file of [mimeTypes]; reports null on cancel. */ +@Composable +expect fun DocumentSelectSingle( + mimeTypes: List, + onPicked: (SelectedMedia?) -> Unit, +) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/wavefront/FakeWaveformAnimation.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/wavefront/FakeWaveformAnimation.kt new file mode 100644 index 0000000000..b445c7c879 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/wavefront/FakeWaveformAnimation.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.ui.components.wavefront + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableFloatState +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn +import kotlin.math.PI +import kotlin.math.sin + +@Preview +@Composable +fun FakeWaveformAnimationPreview() { + val state = + remember { + mutableFloatStateOf((500 % 1000.0f) / 1000.0f) + } + ThemeComparisonColumn { + FakeWaveformAnimation(state, 50, Modifier.width(200.dp)) + } +} + +@Composable +fun FakeWaveformAnimation( + waveformProgress: MutableFloatState, + barCount: Int = 50, + modifier: Modifier = Modifier, +) { + val barBrush = + Brush.linearGradient( + colors = listOf(Color(0xff2598cf), Color(0xff652d80)), + start = Offset(0.2f * 128f, 0f), + end = Offset(0.2f * 128f + 128f, 0f), + ) + + Canvas( + modifier = + modifier + .fillMaxWidth() + .requiredHeight(48.dp) + .padding(horizontal = 10.dp), + ) { + val barWidth = 3.dp.toPx() + val barPadding = 2.dp.toPx() + val barRadius = 2.dp.toPx() + val totalBarWidth = barWidth + barPadding + val startX = (size.width - (barCount * totalBarWidth - barPadding)) / 2f + + for (i in 0 until barCount) { + val phase = (i.toFloat() / barCount) * 2f * PI.toFloat() + val wave = sin(waveformProgress.floatValue * 2f * PI.toFloat() + phase) + val barFraction = ((wave + 1f) / 2f) + val barHeight = size.height * barFraction + val x = startX + i * totalBarWidth + val y = (size.height - barHeight) / 2f + + drawRoundRect( + brush = barBrush, + topLeft = Offset(x, y), + size = Size(barWidth, barHeight), + cornerRadius = CornerRadius(barRadius, barRadius), + style = Fill, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt index ac8b6974b9..ad5b20eccf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt @@ -18,11 +18,8 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.log +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.log -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring @@ -65,7 +62,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -87,6 +83,8 @@ import com.vitorpamplona.amethyst.commons.resources.geocache_log_scan_code import com.vitorpamplona.amethyst.commons.resources.geocache_log_your_log import com.vitorpamplona.amethyst.commons.resources.geocache_unnamed import com.vitorpamplona.amethyst.commons.resources.route_log_geocache_find +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.rememberGeocachePalette @@ -121,7 +119,7 @@ fun LogGeocacheFindScreen( GeocachesFilterAssemblerSubscription(accountViewModel) val address = remember(kind, pubKeyHex, dTag) { Address(kind, pubKeyHex, dTag) } - val model: LogGeocacheFindViewModel = viewModel(key = "LogGeocacheFind-${address.toValue()}") + val model: LogGeocacheFindViewModel = rememberViewModel(key = "LogGeocacheFind-${address.toValue()}") { LogGeocacheFindViewModel() } val scope = rememberCoroutineScope() remember(address) { @@ -132,12 +130,15 @@ fun LogGeocacheFindScreen( val listing = model.listing() var scanning by remember { mutableStateOf(false) } - val photoPicker = - rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - if (uri != null) { - scope.launch { model.uploadImage(uri, null, accountViewModel.host.mediaUploader) } + var pickingPhoto by remember { mutableStateOf(false) } + if (pickingPhoto) { + GallerySelectSingle(imagesOnly = true) { media -> + pickingPhoto = false + if (media != null) { + scope.launch { model.uploadImage(media.uri, media.mimeType, accountViewModel.host.mediaUploader) } } } + } Scaffold( topBar = { @@ -219,7 +220,7 @@ fun LogGeocacheFindScreen( OutlinedButton( enabled = !model.isUploading.value, onClick = { - photoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + pickingPhoto = true }, ) { if (model.isUploading.value) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt index f231ebdc51..d41a08d49f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt @@ -18,9 +18,8 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.log +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.log -import android.net.Uri import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel @@ -28,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -183,7 +183,7 @@ class LogGeocacheFindViewModel : ViewModel() { } suspend fun uploadImage( - uri: Uri, + uri: MediaUri, mimeType: String?, uploader: MediaUploader, ): Boolean { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt index d831b4b5f2..f758022943 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -47,7 +47,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.podcast_edit_show import com.vitorpamplona.amethyst.commons.resources.podcast_publishing_banner @@ -74,6 +73,7 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_show_type_serial import com.vitorpamplona.amethyst.commons.resources.podcast_show_website_label import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar @@ -95,7 +95,7 @@ fun EditPodcastShowScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val vm: EditPodcastShowViewModel = viewModel() + val vm: EditPodcastShowViewModel = rememberViewModel { EditPodcastShowViewModel() } LaunchedEffect(accountViewModel) { vm.init(accountViewModel) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt index 3a7ca74282..bf30181a6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt index d356eab08f..cc3a236e0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt @@ -18,11 +18,8 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -50,12 +47,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -72,6 +67,8 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_upload_hint import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_url_label import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.DocumentSelectSingle +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar @@ -87,7 +84,7 @@ fun NewPodcastTrailerScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val vm: NewPodcastTrailerViewModel = viewModel() + val vm: NewPodcastTrailerViewModel = rememberViewModel { NewPodcastTrailerViewModel() } LaunchedEffect(accountViewModel) { vm.init(accountViewModel) } StrippingFailureDialog(vm.strippingFailureConfirmation) @@ -201,14 +198,6 @@ fun NewPodcastTrailerScreen( } } -// Audio or video file via OpenDocument. +// Audio or video file via the document picker. @Composable -private fun MediaFileSelect(onPicked: (SelectedMedia?) -> Unit) { - val resolver = LocalContext.current.contentResolver - val launcher = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.OpenDocument(), - onResult = { uri: Uri? -> onPicked(uri?.let { SelectedMedia(it, resolver.getType(it)) }) }, - ) - LaunchedEffect(Unit) { launcher.launch(arrayOf("audio/*", "video/*")) } -} +private fun MediaFileSelect(onPicked: (SelectedMedia?) -> Unit) = DocumentSelectSingle(listOf("audio/*", "video/*"), onPicked) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt index a57f1e1f57..d9b5a530a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel @@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.lastPathSegmentOrNull import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nipXXPodcasting20.trailer.Podcasting20TrailerEvent import kotlinx.collections.immutable.persistentListOf @@ -78,7 +79,7 @@ class NewPodcastTrailerViewModel : ViewModel() { return } media.value = MultiOrchestrator(persistentListOf(uri)) - pickedName.value = uri.uri.lastPathSegment?.substringAfterLast('/') + pickedName.value = uri.uri.lastPathSegmentOrNull()?.substringAfterLast('/') pickedMimeType.value = uri.mimeType } @@ -149,7 +150,7 @@ class NewPodcastTrailerViewModel : ViewModel() { _completionEvents.tryEmit(Unit) } catch (t: Throwable) { - accountViewModel.toastManager.toast("Failed to publish trailer", t.message ?: t.javaClass.simpleName) + accountViewModel.toastManager.toast("Failed to publish trailer", t.message ?: t::class.simpleName ?: "") } finally { withContext(Dispatchers.Main.immediate) { isSending.value = false } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt similarity index 75% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt index 43cdbf5921..014e23554a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt @@ -18,13 +18,11 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring -import android.content.Context -import android.media.MediaMetadataRetriever -import android.net.Uri import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.podcasts.rfc2822Date import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator @@ -32,10 +30,9 @@ import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import java.util.UUID +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid /** * Shared upload + media-probe mechanics for the Podcasting-2.0 composers (episode, show, trailer). @@ -97,34 +94,12 @@ object PodcastComposerMedia { val title: String?, ) - /** - * Reads duration + title from a picked audio file via [MediaMetadataRetriever] (heavy — call off - * the main thread). Returns null if the provider rejects it or the file isn't a real container. - */ - fun probeAudio( - context: Context, - uri: Uri, - ): ProbedAudio? { - val retriever = MediaMetadataRetriever() - return try { - retriever.setDataSource(context, uri) - val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() - ProbedAudio( - durationSeconds = durationMs?.let { (it / 1000).toInt().takeIf { secs -> secs > 0 } }, - title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)?.trim()?.ifBlank { null }, - ) - } catch (_: Exception) { - null - } finally { - retriever.release() - } - } - /** Current time as an RFC2822 date string (`Tue, 24 Jun 2025 12:00:00 GMT`) — the spec's `pubdate`. */ - fun rfc2822Now(): String = DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneId.of("GMT"))) + fun rfc2822Now(): String = rfc2822Date(TimeUtils.now()) /** A fresh, stable `d` tag for a new addressable episode/trailer. */ - fun generateDTag(prefix: String): String = "$prefix-${System.currentTimeMillis() / 1000}-${UUID.randomUUID().toString().take(8)}" + @OptIn(ExperimentalUuidApi::class) + fun generateDTag(prefix: String): String = "$prefix-${TimeUtils.now()}-${Uuid.random().toString().take(8)}" /** Splits a comma-separated text field into a clean list (trimmed, no blanks). */ fun parseCsv(text: String): List = text.split(',').map { it.trim() }.filter { it.isNotEmpty() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/V4VSplitEditor.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/V4VSplitEditor.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/V4VSplitEditor.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/V4VSplitEditor.kt index 13be6b657e..ecdbe4a50e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/V4VSplitEditor.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/V4VSplitEditor.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AudioVisualizerSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/AudioVisualizerSettingsScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AudioVisualizerSettingsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/AudioVisualizerSettingsScreen.kt index dc1a974b7e..fb72de7df4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AudioVisualizerSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/AudioVisualizerSettingsScreen.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -64,11 +64,11 @@ import com.vitorpamplona.amethyst.commons.resources.audio_visualizer_settings import com.vitorpamplona.amethyst.commons.resources.audio_visualizer_settings_description import com.vitorpamplona.amethyst.commons.resources.audio_visualizer_static import com.vitorpamplona.amethyst.commons.resources.audio_visualizer_waves +import com.vitorpamplona.amethyst.commons.ui.components.wavefront.FakeWaveformAnimation import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.playback.composable.wavefront.FakeWaveformAnimation import kotlinx.coroutines.flow.Flow @Composable diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.ios.kt index 24c979e0ef..766d009d92 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.ios.kt +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.ios.kt @@ -31,3 +31,12 @@ import kotlinx.collections.immutable.persistentListOf actual fun FileSelect(onFilesSelected: (ImmutableList) -> Unit) { LaunchedEffect(Unit) { onFilesSelected(persistentListOf()) } } + +/** No document picker wired yet: reports a cancel. */ +@Composable +actual fun DocumentSelectSingle( + mimeTypes: List, + onPicked: (SelectedMedia?) -> Unit, +) { + LaunchedEffect(Unit) { onPicked(null) } +} diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.jvm.kt index 24c979e0ef..766d009d92 100644 --- a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.jvm.kt +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.jvm.kt @@ -31,3 +31,12 @@ import kotlinx.collections.immutable.persistentListOf actual fun FileSelect(onFilesSelected: (ImmutableList) -> Unit) { LaunchedEffect(Unit) { onFilesSelected(persistentListOf()) } } + +/** No document picker wired yet: reports a cancel. */ +@Composable +actual fun DocumentSelectSingle( + mimeTypes: List, + onPicked: (SelectedMedia?) -> Unit, +) { + LaunchedEffect(Unit) { onPicked(null) } +}