diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt index 6463ff915f..6785b44283 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt @@ -46,6 +46,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.ui.compose.ContentFrame import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow +import com.vitorpamplona.amethyst.commons.video.isHlsMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.playback.composable.controls.BottomGradientOverlay import com.vitorpamplona.amethyst.service.playback.composable.controls.FullscreenSwipeControlsState @@ -61,7 +62,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.controls.applyView import com.vitorpamplona.amethyst.service.playback.composable.controls.clampViewportShortSide import com.vitorpamplona.amethyst.service.playback.composable.controls.fullscreenSwipeControls import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.LoadedMediaItem -import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia import com.vitorpamplona.amethyst.service.playback.composable.wavefront.AudioPlayingAnimation import com.vitorpamplona.amethyst.service.playback.composable.wavefront.rememberIsAudioTrack diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt index 18348acf29..0761745b1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt @@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.PinBottomIconSize import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.commons.video.isHlsMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.service.cast.CastRequest @@ -80,7 +81,6 @@ import com.vitorpamplona.amethyst.service.cast.resolveCastLiveness import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData -import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia import com.vitorpamplona.amethyst.service.playback.diskCache.HlsLivenessCache import com.vitorpamplona.amethyst.service.playback.pip.PipVideoActivity import com.vitorpamplona.amethyst.ui.cast.CastDevicePickerDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/IsHlsMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/IsHlsMedia.kt deleted file mode 100644 index 464553f440..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/IsHlsMedia.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.service.playback.composable.mediaitem - -import androidx.media3.common.MimeTypes - -/** - * Whether a URL plus its imeta mime identifies HLS. - * - * This is the caller-side form for code that holds a URL and a mime but no `MediaItem` yet — UI that - * has a [MediaItemData] or a `MediaUrlContent`. Once a `MediaItem` exists, - * `isHlsMediaItem` is the equivalent, and both must answer the same way: the UI decides what to - * render, the factory decides how to load it, and a disagreement shows up as a player that streams - * something the surrounding chrome says is a still image. - * - * Defined in terms of [MediaItemCache.toExoPlayerMimeType] rather than re-deriving the rules, so it - * cannot drift from the mime that actually reaches ExoPlayer. That normalizer prefers an explicit - * mime (mapping the four HLS aliases onto [MimeTypes.APPLICATION_M3U8]) and otherwise falls back to a - * **path-anchored** `.m3u8` test. - * - * Both halves matter. A BUD-10 blossom playlist is `https://host/` with no extension at all, - * so only the mime identifies it; and anchoring to the path stops `video.mp4?ref=a.m3u8` counting as - * HLS on the strength of its query string. - * - * Note this answers *is it HLS*, which callers use as a proxy for *is it live*. The proxy is - * imprecise in the same way for every HLS URL — an on-demand HLS playlist also answers true — and - * that imprecision is older than this function. Liveness is only truly knowable from - * `#EXT-X-ENDLIST` once the playlist is loaded, which is what `HlsLivenessCache` records. - */ -fun isHlsMedia( - url: String, - mimeType: String?, -): Boolean = MediaItemCache.toExoPlayerMimeType(mimeType, url) == MimeTypes.APPLICATION_M3U8 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemCache.kt index 9a3096fe8c..4c6c5871fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemCache.kt @@ -25,9 +25,9 @@ import androidx.core.net.toUri import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import androidx.media3.common.MimeTypes import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack import com.vitorpamplona.amethyst.commons.ui.state.GenericBaseCache +import com.vitorpamplona.amethyst.commons.video.normalizeStreamMimeType import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -45,23 +45,7 @@ class MediaItemCache : GenericBaseCache(20) { internal fun toExoPlayerMimeType( mimeType: String?, videoUri: String? = null, - ): String? { - if (!mimeType.isNullOrBlank()) { - return when (mimeType.lowercase()) { - "application/vnd.apple.mpegurl", - "application/x-mpegurl", - "audio/x-mpegurl", - "audio/mpegurl", - -> MimeTypes.APPLICATION_M3U8 - - else -> mimeType - } - } - if (videoUri != null && hasM3u8PathExtension(videoUri)) { - return MimeTypes.APPLICATION_M3U8 - } - return null - } + ): String? = normalizeStreamMimeType(mimeType, videoUri) // A side-loaded track needs its media type declared: a NIP-71 `text-track` says nothing // about the format, and a blossom-hosted one is `https://host/` with no extension @@ -81,14 +65,6 @@ class MediaItemCache : GenericBaseCache(20) { language != null && type != null -> "$language \u00B7 $type" else -> language ?: type } - - // Restrict `.m3u8` matching to the path component so a query param or - // fragment that happens to mention .m3u8 (e.g. `?ref=a.m3u8`) on an - // MP4 URI doesn't misroute to HlsMediaSource. - private fun hasM3u8PathExtension(uri: String): Boolean { - val path = uri.substringBefore('?').substringBefore('#') - return path.endsWith(".m3u8", ignoreCase = true) - } } override suspend fun compute(key: MediaItemData): LoadedMediaItem = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 1e0a0202c3..7a7d55aa09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -26,7 +26,9 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.CompositionLocalProvider import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.ui.components.LocalWindowViewModelStoreOwner import com.vitorpamplona.amethyst.commons.ui.note.elements.NowProvider import com.vitorpamplona.amethyst.debugState import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService @@ -61,7 +63,11 @@ class MainActivity : AppCompatActivity() { setContent { AmethystTheme { NowProvider { - AccountScreen(Amethyst.instance.sessionManager) + // The Activity outlives every nav destination: screens that share state + // across destinations (chess lobby + board, the Cordn group draft) keep it here. + CompositionLocalProvider(LocalWindowViewModelStoreOwner provides this) { + AccountScreen(Amethyst.instance.sessionManager) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index fbb54dbea1..1bca3d9246 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -88,9 +88,9 @@ import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow import com.vitorpamplona.amethyst.commons.ui.components.rememberViewerControlsVisibility import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier +import com.vitorpamplona.amethyst.commons.video.isHlsMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner -import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList 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 b4a8cce6c9..3131255e72 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 @@ -67,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.LocalTabResel import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.TabReselectCoordinator import com.vitorpamplona.amethyst.commons.ui.navigation.findQueryParameterValue import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.ReplyCommentPostScreen +import com.vitorpamplona.amethyst.commons.ui.note.share.ShareNoteAsQrScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.apps.recommendations.ProfileAppRecommendationsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.ArticlesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.BackupConflictReviewScreen @@ -95,7 +96,11 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzInviteScre import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzNewDmScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.JobBoardScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.WorkflowRunBoardScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.CalendarCollectionsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.create.NewCalendarCollectionScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.detail.CalendarEventDetailScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnCreateGroupScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnCreateMembersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupInfoScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnInvitationsScreen @@ -123,6 +128,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata.NewEphemeralChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupBrowseScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupCreateScreen @@ -131,6 +137,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupMembersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupThreadsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.share.ShareToDMScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.ChessGameScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.ChessLobbyScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.CommunityScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.CommunitiesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity.EditCommunityScreen @@ -150,6 +158,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.membersh import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.membershipManagement.MyEmojiListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.favorites.FavoriteAppsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list.FollowPacksScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.create.NewGeocacheScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail.GeocacheDetailScreen 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 @@ -161,6 +171,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.HashtagScre import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.HighlightsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.NewHighlightScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.HomeScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.VoiceReplyScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.nip75Goals.NewGoalScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.display.InterestSetScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.list.ListOfInterestSetsScreen @@ -190,6 +202,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.PodcastsSc 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.PollPostScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.PollsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.results.PollResultsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.privacy.PrivacyOptionsScreen @@ -212,6 +225,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.AudioVisua 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 +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ComposeSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.DrawerSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HiddenWordsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HomeTabsSettingsScreen @@ -279,24 +293,16 @@ import com.vitorpamplona.amethyst.ui.note.PayViaIntentScreen import com.vitorpamplona.amethyst.ui.note.UpdateReactionTypeScreen import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageFileScreen import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageScreen -import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsQrScreen import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.WebAppScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarCollectionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarReminderSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.create.NewCalendarEventScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detail.CalendarEventDetailScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateGroupScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateMembersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroupChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChannelListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessGameScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessLobbyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabAccountWatcher import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabLayer import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabPreloader @@ -305,15 +311,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.FavoriteAppManifestPr import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.NostrAppScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.FollowPackFeedScreen 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.gitRepo.GitNewIssueScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryCodeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryIssuesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryPullsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.VoiceReplyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.AddToMusicPlaylistSheet import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.NewMusicTrackScreen @@ -322,13 +324,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.NappletsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.NewPodcastEpisodeScreen -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 import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ScanQrImageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86.RelayManagementScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.ScheduledPostsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ComposeSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NotificationSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.OtsSettingsScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt index a218c7d509..456783c0cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt @@ -93,6 +93,7 @@ import com.vitorpamplona.amethyst.commons.resources.share_as_image_watermark import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.commons.ui.components.LoadNote import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer @@ -103,7 +104,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt index 60428dea67..46d47a6e65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt @@ -48,13 +48,13 @@ import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarAppointmentLines import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarDateBadge import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detailRouteFor -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt index 4648940283..6f51e05d79 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt @@ -60,9 +60,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.ChessViewModelFactory import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelFactory import com.vitorpamplona.quartz.nip64Chess.challenge.offer.LiveChessGameChallengeEvent import com.vitorpamplona.quartz.nip64Chess.end.LiveChessGameEndEvent import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index 137fc0b083..745a0d2a72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size75Modifier import com.vitorpamplona.amethyst.commons.ui.theme.VoiceHeightModifier import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier +import com.vitorpamplona.amethyst.commons.video.isHlsMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController @@ -74,7 +75,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.controls.PictureIn import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.LoadedMediaItem import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData -import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia import com.vitorpamplona.amethyst.service.playback.composable.wavefront.Waveform import com.vitorpamplona.amethyst.service.playback.pip.PipVideoActivity import com.vitorpamplona.amethyst.ui.components.ShareMediaAction diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt index e1c361343c..1e38a05e73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt @@ -99,6 +99,8 @@ class AndroidAccountViewModelHost( ): Result = VoiceAnonymizer().anonymize(input.toFile(), VoicePreset.valueOf(presetName)) // Built on the application context: the assistant outlives any one Activity inside the ViewModel. + override val supportsWritingAssistant: Boolean get() = WritingAssistantFactory.IS_SUPPORTED + override fun createWritingAssistant(): WritingAssistant = WritingAssistantFactory.create(modules.appContext) override val savedAccounts: Flow> = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt index 48922d31f1..52947ae777 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry import com.vitorpamplona.amethyst.commons.model.navigation.NavBarItem import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.datasource.ArticlesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.datasource.BadgesFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.datasource.CommunitiesListFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.dataso import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.workouts.datasource.WorkoutsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssemblerSubscription diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt index b08505e1bc..705333eca4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsScreen.kt index 0403996741..b43337ca94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsScreen.kt @@ -39,8 +39,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import kotlinx.coroutines.launch @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/IcsShare.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/IcsShare.kt deleted file mode 100644 index fad8c29ef1..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/IcsShare.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.calendars - -import android.content.Context -import android.content.Intent -import androidx.core.content.FileProvider -import java.io.File - -/** - * Writes [content] to a `.ics` file in the app's cache dir and opens the system share sheet so - * the user can hand the file to a calendar app, email client, file manager, etc. - * - * The file lives in `cacheDir/calendar/` — covered by the existing `` entry in - * `file_paths.xml`, so [FileProvider] can hand out a `content://` URI without further config. - * The receiver gets a read-permission grant via [Intent.FLAG_GRANT_READ_URI_PERMISSION] that - * lasts only for the duration of the share. - */ -fun shareIcs( - context: Context, - filename: String, - content: String, - chooserTitle: String, -) { - val dir = File(context.cacheDir, "calendar").apply { mkdirs() } - val file = File(dir, filename) - file.writeText(content) - - val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file) - val intent = - Intent(Intent.ACTION_SEND).apply { - type = "text/calendar" - putExtra(Intent.EXTRA_STREAM, uri) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - val chooser = - Intent - .createChooser(intent, chooserTitle) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(chooser) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt index a0e91364df..1d6270286f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt @@ -38,10 +38,10 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.new_chess_game import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.ChessViewModelFactory import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelFactory /** * Floating action button for creating new chess game challenges diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt index 6807e8465a..0b56c3bddb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.note.CheckHiddenFeedWatchBlockAndReport import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.GalleryThumbnail import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/QrPayloadTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/QrPayloadTest.kt index e00804aded..0e5a42badd 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/QrPayloadTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/QrPayloadTest.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.note.share import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.ui.note.share.QrPayloadMode +import com.vitorpamplona.amethyst.commons.ui.note.share.qrPayloadFor import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal diff --git a/commons/plans/2026-09-27-one-ui-android-desktop.md b/commons/plans/2026-09-27-one-ui-android-desktop.md index bdbbf41bfc..891b398efc 100644 --- a/commons/plans/2026-09-27-one-ui-android-desktop.md +++ b/commons/plans/2026-09-27-one-ui-android-desktop.md @@ -466,7 +466,18 @@ assemblers, `EventSync`, …). Packages are renamed on the way: wraps BiometricPrompt with the keyguard fallback. `CalendarTimeFormat` is shared on `DateSkeletonFormatter`. Moved: search, the long-form composer, the calendar collection editor, NWC setup and the zap-amount settings. 185 of 252 screens shared. - - **Next:** 67 screens remain, and what blocks them is the platform itself: + - **Wave 10 (2026-09-30):** Home, Discover, the nest lobby and the Marmot group editor + first; then the date pickers on `LocalClock` (poll and zap-poll deadlines, schedule-at + with its presets), `FileSharer.shareTextFile` (the .ics export), `PhoneCalendar` (the + system event composer), `relativeTimeSpan` (`DateUtils` / `NSRelativeDateTimeFormatter`), + `rememberWindowViewModel` + `LocalWindowViewModelStoreOwner` (state shared across + destinations: the chess lobby and board, the Cordn group draft), `isHlsMedia` in + `commons.video`, `ChessEventBroadcaster` in commonMain and `supportsWritingAssistant` on + the host. `geo:` links go through `LocalUriHandler`. Moved: the note and poll composers, + voice reply, the calendar collections and event detail, the geocache detail and editor, + the live-activity channel, chess, Cordn group creation, the QR share and the compose + settings. 204 of 252 screens shared. + - **Next:** 48 screens remain, and what blocks them is the platform itself: - `ReactionsRow` (2.7k lines: pay-to-app, voice replies, wallet intents). - The Nests activity and PiP. - The call screens. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessDismissedGamesStore.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessDismissedGamesStore.kt index 1adbf9ab06..b895dfd75a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessDismissedGamesStore.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessDismissedGamesStore.kt @@ -25,6 +25,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.core.stringSetPreferencesKey +import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first import okio.IOException @@ -64,3 +65,11 @@ class ChessDismissedGamesStore( } } } + +/** + * Where the app keeps the dismissed-chess-games store. + * + * A new file rather than a migration of `chess_dismissed_games`: anyone who had dismissed a game + * sees it once more and dismisses it again. + */ +fun AppPreferenceStores.chessDismissedGamesStore(): ChessDismissedGamesStore = ChessDismissedGamesStore(getDataStore("chess_dismissed_games_v2")) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt similarity index 94% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt index 47a66c3f6c..c157b01c38 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip64Chess.jester.JesterProtocol import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.currentTimeMillis import kotlinx.coroutines.delay /** @@ -84,7 +85,7 @@ class ChessEventBroadcaster( val dummyFilter = Filter( kinds = listOf(JesterProtocol.KIND), - since = (System.currentTimeMillis() / 1000) + 3600, // 1 hour in future = no results + since = (currentTimeMillis() / 1000) + 3600, // 1 hour in future = no results limit = 1, ) @@ -136,10 +137,10 @@ class ChessEventBroadcaster( targetRelays: Set, timeoutMs: Long, ): Set { - val startTime = System.currentTimeMillis() + val startTime = currentTimeMillis() val pollInterval = 200L - while (System.currentTimeMillis() - startTime < timeoutMs) { + while (currentTimeMillis() - startTime < timeoutMs) { val connected = client.connectedRelaysFlow().value val targetConnected = targetRelays.intersect(connected) @@ -148,7 +149,7 @@ class ChessEventBroadcaster( } // At least one connected is good enough after half the timeout - if (targetConnected.isNotEmpty() && System.currentTimeMillis() - startTime > timeoutMs / 2) { + if (targetConnected.isNotEmpty() && currentTimeMillis() - startTime > timeoutMs / 2) { return targetConnected } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/video/HlsMedia.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/video/HlsMedia.kt new file mode 100644 index 0000000000..7918b678e3 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/video/HlsMedia.kt @@ -0,0 +1,75 @@ +/* + * 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.video + +/** The mime HLS playlists are normalized to — the value of media3's `MimeTypes.APPLICATION_M3U8`. */ +const val HLS_MIME_TYPE = "application/x-mpegURL" + +/** + * The mime a player should be handed for [mimeType] on [url]: an explicit mime wins (the four HLS + * aliases map onto [HLS_MIME_TYPE]); without one, a path-anchored `.m3u8` means HLS; otherwise + * null, letting the player infer the format. + */ +fun normalizeStreamMimeType( + mimeType: String?, + url: String? = null, +): String? { + if (!mimeType.isNullOrBlank()) { + return when (mimeType.lowercase()) { + "application/vnd.apple.mpegurl", + "application/x-mpegurl", + "audio/x-mpegurl", + "audio/mpegurl", + -> HLS_MIME_TYPE + + else -> mimeType + } + } + if (url != null && hasM3u8PathExtension(url)) { + return HLS_MIME_TYPE + } + return null +} + +/** + * Whether a URL plus its imeta mime identifies HLS, for code that holds a URL and a mime but no + * player item yet. Defined through [normalizeStreamMimeType] so it cannot drift from the mime that + * actually reaches the player. + * + * Both halves matter. A BUD-10 blossom playlist is `https://host/` with no extension at all, + * so only the mime identifies it; and anchoring to the path stops `video.mp4?ref=a.m3u8` counting as + * HLS on the strength of its query string. + * + * Note this answers *is it HLS*, which callers use as a proxy for *is it live*: an on-demand HLS + * playlist also answers true. Liveness is only truly knowable from `#EXT-X-ENDLIST` once the + * playlist is loaded. + */ +fun isHlsMedia( + url: String, + mimeType: String?, +): Boolean = normalizeStreamMimeType(mimeType, url) == HLS_MIME_TYPE + +// Restrict `.m3u8` matching to the path component so a query param or fragment that happens to +// mention .m3u8 (e.g. `?ref=a.m3u8`) on an MP4 URI doesn't misroute to the HLS source. +private fun hasM3u8PathExtension(uri: String): Boolean { + val path = uri.substringBefore('?').substringBefore('#') + return path.endsWith(".m3u8", ignoreCase = true) +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt index 4e6967c763..e5d4bca6d6 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt @@ -91,6 +91,9 @@ interface AccountViewModelHost { presetName: String, ): Result = Result.failure(UnsupportedOperationException("Voice anonymization is not available on this platform")) + /** Whether this build can offer an on-device writing assistant at all (it shows the setting). */ + val supportsWritingAssistant: Boolean get() = false + /** A new on-device writing assistant, or null where the platform has none. */ fun createWritingAssistant(): WritingAssistant? = null diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/IsHlsMediaTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/video/IsHlsMediaTest.kt similarity index 89% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/IsHlsMediaTest.kt rename to commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/video/IsHlsMediaTest.kt index 9b34587518..14e69d1f8c 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/IsHlsMediaTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/video/IsHlsMediaTest.kt @@ -18,11 +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.service.playback.composable.mediaitem +package com.vitorpamplona.amethyst.commons.video -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue /** * The two cases that motivated replacing the old `.m3u8`-substring predicate are @@ -39,7 +39,7 @@ class IsHlsMediaTest { assertTrue(isHlsMedia(blossom, "application/x-mpegurl")) assertTrue(isHlsMedia(blossom, "application/vnd.apple.mpegurl")) - assertFalse("no mime and no extension leaves nothing to go on", isHlsMedia(blossom, null)) + assertFalse(isHlsMedia(blossom, null), "no mime and no extension leaves nothing to go on") } @Test @@ -54,7 +54,7 @@ class IsHlsMediaTest { fun recognisesAPlainM3u8Path() { assertTrue(isHlsMedia("https://host/live.m3u8", null)) assertTrue(isHlsMedia("https://host/live.M3U8", null)) - assertTrue("query strings don't hide the path", isHlsMedia("https://host/live.m3u8?vt=abc", null)) + assertTrue(isHlsMedia("https://host/live.m3u8?vt=abc", null), "query strings don't hide the path") } @Test diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.android.kt index 6066b0f73c..50c6065060 100644 --- a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.android.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.android.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.ui.components +import android.content.Context import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -33,29 +34,62 @@ import java.util.zip.ZipOutputStream @Composable actual fun rememberFileSharer(): FileSharer { val context = LocalContext.current - return remember(context) { - FileSharer { zipName, files, title -> - val zipFile = File(context.cacheDir, zipName) - ZipOutputStream(FileOutputStream(zipFile)).use { zip -> - files.forEach { (name, content) -> - zip.putNextEntry(ZipEntry(name)) - zip.write(content.toByteArray()) - zip.closeEntry() - } + return remember(context) { AndroidFileSharer(context) } +} + +private class AndroidFileSharer( + val context: Context, +) : FileSharer { + override fun shareTextFilesAsZip( + zipName: String, + files: List>, + title: String, + ) { + val zipFile = File(context.cacheDir, zipName) + ZipOutputStream(FileOutputStream(zipFile)).use { zip -> + files.forEach { (name, content) -> + zip.putNextEntry(ZipEntry(name)) + zip.write(content.toByteArray()) + zip.closeEntry() + } + } + + val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", zipFile) + + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "application/zip" + putExtra(Intent.EXTRA_STREAM, uri) + putExtra(Intent.EXTRA_TITLE, title) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", zipFile) + context.startActivity(Intent.createChooser(sendIntent, title)) + } - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "application/zip" - putExtra(Intent.EXTRA_STREAM, uri) - putExtra(Intent.EXTRA_TITLE, title) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } + /** + * The file lives in `cacheDir/shared/`, covered by the `` entry in + * `file_paths.xml`; the receiver's read grant lasts only for the share. + */ + override fun shareTextFile( + fileName: String, + mimeType: String, + content: String, + title: String, + ) { + val dir = File(context.cacheDir, "shared").apply { mkdirs() } + val file = File(dir, fileName) + file.writeText(content) - context.startActivity(Intent.createChooser(sendIntent, title)) - } + val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file) + val intent = + Intent(Intent.ACTION_SEND).apply { + type = mimeType + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(Intent.createChooser(intent, title).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/AddToPhoneCalendar.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.android.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/AddToPhoneCalendar.kt rename to commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.android.kt index 11a93892f0..00a73473bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/AddToPhoneCalendar.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.android.kt @@ -18,17 +18,26 @@ * 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.calendars +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.provider.CalendarContract +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext import com.vitorpamplona.amethyst.commons.model.nip52Calendar.parseIsoDateToUnixSeconds import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent +@Composable +actual fun rememberPhoneCalendar(): PhoneCalendar { + val context = LocalContext.current + return remember(context) { PhoneCalendar { addToPhoneCalendar(context, it) } } +} + /** * Opens the system "New Event" composer (Google Calendar / Samsung / iCloud / etc.) pre-populated * with this appointment's title, time, location, and description. The user sees their normal @@ -38,7 +47,7 @@ import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent * Returns true if a calendar app handled the intent, false if no handler was found — the caller * can decide whether to fall back to the .ics share path. */ -fun addToPhoneCalendar( +private fun addToPhoneCalendar( context: Context, event: Event, ): Boolean { diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.android.kt new file mode 100644 index 0000000000..36dad1e866 --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.android.kt @@ -0,0 +1,36 @@ +/* + * 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.screen.loggedIn.calendars + +import android.text.format.DateUtils + +actual suspend fun relativeTimeSpan( + targetSeconds: Long, + nowSeconds: Long, + dayResolution: Boolean, +): String = + DateUtils + .getRelativeTimeSpanString( + targetSeconds * 1000L, + nowSeconds * 1000L, + if (dayResolution) DateUtils.DAY_IN_MILLIS else DateUtils.MINUTE_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/FileServerSelectionRow.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/mediaServers/FileServerSelectionRow.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/FileServerSelectionRow.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/mediaServers/FileServerSelectionRow.kt index cc37db7d72..98ab16054c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/FileServerSelectionRow.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/mediaServers/FileServerSelectionRow.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.actions.mediaServers +package com.vitorpamplona.amethyst.commons.ui.actions.mediaServers import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/UploadProgressIndicator.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/UploadProgressIndicator.kt index 0a8c4439ea..0da947cfed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/UploadProgressIndicator.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.actions.uploads +package com.vitorpamplona.amethyst.commons.ui.actions.uploads import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VoiceAnonymizationSection.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VoiceAnonymizationSection.kt index 28b2cfe15a..8f10a60d63 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VoiceAnonymizationSection.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.actions.uploads +package com.vitorpamplona.amethyst.commons.ui.actions.uploads import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VoicePresetSelector.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VoicePresetSelector.kt index 3e0ca3fbd4..4a9faa8802 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VoicePresetSelector.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.actions.uploads +package com.vitorpamplona.amethyst.commons.ui.actions.uploads import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -32,7 +32,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.audio.VoicePreset import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -44,7 +43,6 @@ fun VoicePresetSelector( onPresetSelected: (VoicePreset) -> Unit, modifier: Modifier = Modifier, ) { - val context = LocalContext.current val isProcessing = processingPreset != null Row( diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.kt index 544103e9cc..10d368ec52 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.runtime.Composable /** Hands generated files to the platform's share sheet. */ -fun interface FileSharer { +interface FileSharer { /** * Packs [files] (name to text content) into a zip named [zipName] and shares it, behind a * chooser titled [title] where the platform has one. @@ -33,6 +33,17 @@ fun interface FileSharer { files: List>, title: String, ) + + /** + * Writes [content] to a file named [fileName] and shares it as [mimeType], behind a chooser + * titled [title] where the platform has one. + */ + fun shareTextFile( + fileName: String, + mimeType: String, + content: String, + title: String, + ) } /** diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.kt index 5542f13703..0112f8656c 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.kt @@ -67,6 +67,7 @@ inline fun rememberViewModel( * when the store has no view model yet. */ @Composable +@PublishedApi internal fun rememberLambdaFactory(factory: () -> VM): ViewModelProvider.Factory { val currentFactory by rememberUpdatedState(factory) return remember { diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/WindowViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/WindowViewModel.kt new file mode 100644 index 0000000000..f2084bd3c6 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/WindowViewModel.kt @@ -0,0 +1,64 @@ +/* + * 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 + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import kotlin.reflect.KClass + +/** + * The store that outlives every navigation destination in this window: Android's Activity. The + * platform entry point provides it above the nav host, whose destinations each get a store of + * their own. + */ +val LocalWindowViewModelStoreOwner: ProvidableCompositionLocal = staticCompositionLocalOf { null } + +/** + * A view model shared by every destination of this window, for state that has to survive + * navigating between screens (a draft filled in over two screens, a game the lobby and the board + * both watch). Without a window store it falls back to [rememberViewModel], scoped to the caller. + */ +@Composable +fun rememberWindowViewModel( + modelClass: KClass, + key: String, + factory: ViewModelProvider.Factory, +): VM { + val owner = LocalWindowViewModelStoreOwner.current ?: return rememberViewModel(modelClass, key, factory) + return remember(owner, key) { ViewModelProvider.create(owner, factory).get(key, modelClass) } +} + +@Composable +inline fun rememberWindowViewModel( + key: String, + factory: ViewModelProvider.Factory, +): VM = rememberWindowViewModel(VM::class, key, factory) + +@Composable +inline fun rememberWindowViewModel( + key: String, + noinline factory: () -> VM, +): VM = rememberWindowViewModel(VM::class, key, rememberLambdaFactory(factory)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/polls/PollDeadlinePicker.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/polls/PollDeadlinePicker.kt index db77c424f5..9c08df9531 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/polls/PollDeadlinePicker.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.note.creators.polls +package com.vitorpamplona.amethyst.commons.ui.note.creators.polls import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/polls/PollOptionsField.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/polls/PollOptionsField.kt index 3d071a9286..6346c97147 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/polls/PollOptionsField.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.note.creators.polls +package com.vitorpamplona.amethyst.commons.ui.note.creators.polls import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/scheduling/ScheduleAtPicker.kt similarity index 86% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/scheduling/ScheduleAtPicker.kt index b7b183eedd..ee9973ce6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/scheduling/ScheduleAtPicker.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.note.creators.scheduling +package com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling -import android.text.format.DateFormat import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -54,7 +53,6 @@ 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.unit.dp import androidx.compose.ui.unit.sp @@ -76,18 +74,15 @@ import com.vitorpamplona.amethyst.commons.resources.schedule_post_time_label import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_multi import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_single import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_title +import com.vitorpamplona.amethyst.commons.search.calendar.LocalClock +import com.vitorpamplona.amethyst.commons.search.calendar.SearchDate +import com.vitorpamplona.amethyst.commons.ui.note.rememberIs24HourClock import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.quartz.utils.TimeUtils -import java.time.DayOfWeek -import java.time.Instant -import java.time.LocalDate -import java.time.LocalTime -import java.time.ZoneId -import java.time.ZoneOffset -import java.time.temporal.TemporalAdjusters +import com.vitorpamplona.quartz.utils.currentTimeMillis /** * Two-stage date + time picker for scheduling a post for future publication. @@ -107,29 +102,26 @@ fun ScheduleAtPicker( var showDatePicker by remember { mutableStateOf(false) } var showTimePicker by remember { mutableStateOf(false) } - val currentTime = - Instant - .ofEpochMilli(scheduledForSec * 1000) - .atZone(ZoneId.systemDefault()) - .toLocalDateTime() + // The scheduled time as a local wall-clock time: seconds since the epoch shifted by this zone's offset. + val localSeconds = scheduledForSec + LocalClock.utcOffsetSeconds(scheduledForSec) + val localSecondOfDay = localSeconds.mod(TimeUtils.ONE_DAY) + val currentYear = SearchDate.civilFromDays(localSeconds.floorDiv(TimeUtils.ONE_DAY)).year val datePickerState = rememberDatePickerState( initialSelectedDateMillis = scheduledForSec * 1000, - yearRange = currentTime.year..2050, + yearRange = currentYear..2050, selectableDates = object : SelectableDates { - override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= System.currentTimeMillis() - 86_400_000 + override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= currentTimeMillis() - 86_400_000 }, ) - val context = LocalContext.current - val timePickerState = rememberTimePickerState( - initialHour = currentTime.hour, - initialMinute = currentTime.minute, - is24Hour = DateFormat.is24HourFormat(context), + initialHour = localSecondOfDay / TimeUtils.ONE_HOUR, + initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE, + is24Hour = rememberIs24HourClock(), ) Column(Modifier.fillMaxWidth()) { @@ -220,8 +212,7 @@ fun ScheduleAtPicker( (timePickerState.minute * TimeUtils.ONE_MINUTE) } ?: TimeUtils.oneDayAhead() - val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now()) - val rawSec = datetimeLocalTimeZone - offset.totalSeconds + val rawSec = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now()) onChanged(roundUpToNextQuarterHour(rawSec)) showTimePicker = false @@ -300,24 +291,22 @@ private fun PresetChips(onPick: (Long) -> Unit) { } } -private fun presetInOneHour(): Long = (System.currentTimeMillis() / 1000) + 3600 +private fun presetInOneHour(): Long = (currentTimeMillis() / 1000) + 3600 -private fun presetTomorrowMorning(): Long { - val zone = ZoneId.systemDefault() - val tomorrow9am = LocalDate.now(zone).plusDays(1).atTime(LocalTime.of(9, 0)) - return tomorrow9am.atZone(zone).toEpochSecond() -} +private fun presetTomorrowMorning(): Long = nineAm(LocalClock.today().plusDays(1)) private fun presetNextMondayMorning(): Long { - val zone = ZoneId.systemDefault() // Always step at least one day forward — if today is Monday, return next Monday. - val target = - LocalDate - .now(zone) - .plusDays(1) - .with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY)) - .atTime(LocalTime.of(9, 0)) - return target.atZone(zone).toEpochSecond() + val tomorrow = LocalClock.today().plusDays(1) + val monday = 1 + return nineAm(tomorrow.plusDays((monday - tomorrow.dayOfWeek()).mod(7))) +} + +/** 09:00 local time on [date], read with the offset in force at 09:00, not at midnight. */ +private fun nineAm(date: SearchDate): Long { + val midnight = LocalClock.startOfDay(date) + val guess = midnight + 9 * TimeUtils.ONE_HOUR + return guess + LocalClock.utcOffsetSeconds(midnight) - LocalClock.utcOffsetSeconds(guess) } /** @@ -326,9 +315,9 @@ private fun presetNextMondayMorning(): Long { * (rare — only if the user picks the exact current quarter-hour), bump forward * one slot. */ -internal fun roundUpToNextQuarterHour(epochSec: Long): Long { +fun roundUpToNextQuarterHour(epochSec: Long): Long { val quarter = 15 * 60L val rounded = ((epochSec + quarter - 1) / quarter) * quarter - val nowSec = System.currentTimeMillis() / 1000 + val nowSec = currentTimeMillis() / 1000 return if (rounded <= nowSec) rounded + quarter else rounded } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt similarity index 85% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt index 5b80cbd6ae..c12c3eadea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollDeadlinePicker.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.note.creators.zappolls +package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls -import android.text.format.DateFormat import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -45,7 +44,6 @@ 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.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -57,13 +55,14 @@ import com.vitorpamplona.amethyst.commons.resources.confirm import com.vitorpamplona.amethyst.commons.resources.next import com.vitorpamplona.amethyst.commons.resources.poll_closing_date_time import com.vitorpamplona.amethyst.commons.resources.poll_closing_in +import com.vitorpamplona.amethyst.commons.search.calendar.LocalClock +import com.vitorpamplona.amethyst.commons.search.calendar.SearchDate +import com.vitorpamplona.amethyst.commons.ui.note.rememberIs24HourClock import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.quartz.utils.TimeUtils -import java.time.Instant -import java.time.ZoneId -import java.time.ZoneOffset +import com.vitorpamplona.quartz.utils.currentTimeMillis @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -72,26 +71,28 @@ fun ZapPollDeadlinePicker(model: ShortNotePostViewModel) { var showTimePicker by remember { mutableStateOf(false) } // Get current time details - val currentTime = Instant.ofEpochMilli(model.zapPollClosedAt * 1000).atZone(ZoneId.systemDefault()).toLocalDateTime() + // The deadline as a local wall-clock time: seconds since the epoch shifted by this zone's offset. + val localSeconds = model.zapPollClosedAt + LocalClock.utcOffsetSeconds(model.zapPollClosedAt) + val localSecondOfDay = localSeconds.mod(TimeUtils.ONE_DAY) + val currentYear = SearchDate.civilFromDays(localSeconds.floorDiv(TimeUtils.ONE_DAY)).year val datePickerState = rememberDatePickerState( initialSelectedDateMillis = model.zapPollClosedAt * 1000, - yearRange = currentTime.year..2050, + yearRange = currentYear..2050, selectableDates = object : SelectableDates { override fun isSelectableDate(utcTimeMillis: Long): Boolean { // Only allow today and future dates - return utcTimeMillis >= System.currentTimeMillis() - 86400000 // minus 24h buffer + return utcTimeMillis >= currentTimeMillis() - 86400000 // minus 24h buffer } }, ) - val context = LocalContext.current val timePickerState = rememberTimePickerState( - initialHour = currentTime.hour, - initialMinute = currentTime.minute, - is24Hour = DateFormat.is24HourFormat(context), + initialHour = localSecondOfDay / TimeUtils.ONE_HOUR, + initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE, + is24Hour = rememberIs24HourClock(), ) OutlinedCard( @@ -148,10 +149,7 @@ fun ZapPollDeadlinePicker(model: ShortNotePostViewModel) { (timePickerState.minute * TimeUtils.ONE_MINUTE) } ?: TimeUtils.oneDayAhead() - // Get the offset from UTC for the current instant in the local time zone - val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now()) - - model.zapPollClosedAt = datetimeLocalTimeZone - offset.totalSeconds + model.zapPollClosedAt = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now()) showTimePicker = false }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollField.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollField.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollField.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollField.kt index 6227329a10..67c32e7e99 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollField.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollField.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.note.creators.zappolls +package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollOption.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollOption.kt index c634c455a9..a6fad228b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollOption.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.note.creators.zappolls +package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls import androidx.compose.foundation.layout.Row import androidx.compose.foundation.text.KeyboardOptions @@ -67,7 +67,7 @@ fun ZapPollOption( }, label = { Text( - text = stringRes(Res.string.poll_option_index).format(optionIndex + 1), + text = stringRes(Res.string.poll_option_index, (optionIndex + 1).toString()), color = MaterialTheme.colorScheme.placeholderText, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollVoteValueRange.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollVoteValueRange.kt index f9770d7e7a..9166ab3d9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollVoteValueRange.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.note.creators.zappolls +package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/QrPayload.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/QrPayload.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/QrPayload.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/QrPayload.kt index 63ae327439..20a93d384d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/QrPayload.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/QrPayload.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.note.share +package com.vitorpamplona.amethyst.commons.ui.note.share import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.ui.note.externalLinkForNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/ShareNoteAsQrScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/ShareNoteAsQrScreen.kt index 0932556399..b8a5a2216e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/ShareNoteAsQrScreen.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.note.share +package com.vitorpamplona.amethyst.commons.ui.note.share import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/SharedNoteCard.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/SharedNoteCard.kt index bf1e6a0050..aa7ac4bf7b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/SharedNoteCard.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.note.share +package com.vitorpamplona.amethyst.commons.ui.note.share import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -57,10 +57,10 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningReasons import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.NoteAuthorPicture +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.GalleryThumbnail +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.UrlImageView import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.GalleryThumbnail -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.UrlImageView import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsScreen.kt index 294dd1f135..783e53c1ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsScreen.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.calendars +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -32,8 +32,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription /** * Top-level screen for browsing NIP-52 kind-31924 calendars (collections of appointments). Reuses diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt index 380ea64ce6..7b7e4ff0f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.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.calendars +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt index 6c399baa03..c5058fd7fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsView.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.calendars +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -42,7 +42,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember 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.style.TextOverflow import androidx.compose.ui.unit.dp @@ -62,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_empty_collections_s import com.vitorpamplona.amethyst.commons.resources.calendar_empty_collections_title import com.vitorpamplona.amethyst.commons.resources.calendar_export_event import com.vitorpamplona.amethyst.commons.resources.calendar_export_share_title +import com.vitorpamplona.amethyst.commons.ui.components.rememberFileSharer import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop @@ -157,7 +157,7 @@ fun CalendarCollectionCard( val title = remember(note.idHex) { event.title() } val description = remember(note.idHex) { event.content.take(180) } val count = remember(note.idHex) { event.calendarEventAddresses().size } - val context = LocalContext.current + val fileSharer = rememberFileSharer() val icsChooserTitle = stringRes(Res.string.calendar_export_share_title) // Calendar collections are addressable (kind 31924); route to the dedicated @@ -223,7 +223,7 @@ fun CalendarCollectionCard( val members = collectMembers(event) val ics = IcsExport.calendarToIcs(event, members, TimeUtils.now()) val filename = IcsExport.calendarFilename(event) - shareIcs(context, filename, ics, icsChooserTitle) + fileSharer.shareTextFile(filename, "text/calendar", ics, icsChooserTitle) }) { Icon( symbol = MaterialSymbols.Share, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarRelativeTime.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarRelativeTime.kt similarity index 77% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarRelativeTime.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarRelativeTime.kt index 5549052e62..d3b0fb32df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarRelativeTime.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarRelativeTime.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.calendars +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars -import android.text.format.DateUtils import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState @@ -29,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.calendar_relative_ongoing import com.vitorpamplona.amethyst.commons.resources.calendar_relative_ongoing_with_end import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.relativeTimeSpan import com.vitorpamplona.quartz.utils.TimeUtils /** @@ -36,14 +36,14 @@ import com.vitorpamplona.quartz.utils.TimeUtils * label for an appointment. Returns null when the event has no parseable start (nothing to * anchor a relative phrase to). * - * Uses [DateUtils.getRelativeTimeSpanString] for the underlying minute/hour/day phrasing — that + * Uses the platform's relative-time formatter ([relativeTimeSpan]) for the underlying minute/hour/day phrasing — that * helper is locale-aware and ages from "just now" through "in N days" to absolute date for * far-out events. For all-day events we extend the resolution to DAY so we get "tomorrow", * "in 3 days" instead of an hour-precision phrase that would lie about the start moment. * * Ongoing events (start ≤ now ≤ end) get a composite "Happening now · ends in X" so a user * mid-event sees how much time is left rather than the misleading "started X minutes ago" that - * DateUtils would produce on its own. + * the formatter would produce on its own. */ suspend fun relativeTimeLabel( view: CalendarAppointmentView, @@ -54,31 +54,11 @@ suspend fun relativeTimeLabel( if (end != null && start <= nowSeconds && nowSeconds <= end) { val ongoing = loadStringRes(Res.string.calendar_relative_ongoing) - val endsIn = - DateUtils - .getRelativeTimeSpanString( - end * 1000L, - nowSeconds * 1000L, - if (view.isAllDay) DateUtils.DAY_IN_MILLIS else DateUtils.MINUTE_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE, - ).toString() + val endsIn = relativeTimeSpan(end, nowSeconds, view.isAllDay) return loadStringRes(Res.string.calendar_relative_ongoing_with_end, ongoing, endsIn) } - val minResolution = - if (view.isAllDay) { - DateUtils.DAY_IN_MILLIS - } else { - DateUtils.MINUTE_IN_MILLIS - } - - return DateUtils - .getRelativeTimeSpanString( - start * 1000L, - nowSeconds * 1000L, - minResolution, - DateUtils.FORMAT_ABBREV_RELATIVE, - ).toString() + return relativeTimeSpan(start, nowSeconds, view.isAllDay) } /** diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.kt new file mode 100644 index 0000000000..f2e91e24f6 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.kt @@ -0,0 +1,41 @@ +/* + * 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.screen.loggedIn.calendars + +import androidx.compose.runtime.Composable +import com.vitorpamplona.quartz.nip01Core.core.Event + +/** Hands a NIP-52 appointment to the device's own calendar app. */ +fun interface PhoneCalendar { + /** + * Opens the calendar's "New Event" composer pre-filled with [event]. Returns false when there + * is no calendar app to take it (or [event] is not an appointment), so the caller can fall + * back to sharing an .ics file. + */ + fun add(event: Event): Boolean +} + +/** + * Android opens the system calendar composer. Desktop and iOS have no calendar composer wired + * yet and always report false. + */ +@Composable +expect fun rememberPhoneCalendar(): PhoneCalendar diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.kt similarity index 61% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.kt index 8aa803bd54..4f290044c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.kt @@ -18,18 +18,16 @@ * 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.chess - -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars /** - * Where Android keeps the dismissed-chess-games store. - * - * A new file rather than a migration of `chess_dismissed_games`: the dismissed - * list is a convenience, and chess has few enough users that carrying the old - * data over is not worth the code. Anyone who had dismissed a game sees it once - * more and dismisses it again. + * The platform's own phrase for how far [targetSeconds] is from [nowSeconds] ("in 2 hr.", + * "5 min. ago", "tomorrow"), no finer than a day when [dayResolution] is set. Android asks + * `DateUtils`, iOS `NSRelativeDateTimeFormatter`; Desktop has no such formatter and writes a + * compact signed span. */ -internal fun chessDismissedGamesData(stores: AppPreferenceStores): DataStore = stores.getDataStore("chess_dismissed_games_v2") +expect suspend fun relativeTimeSpan( + targetSeconds: Long, + nowSeconds: Long, + dayResolution: Boolean, +): String diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/datasource/CalendarsFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/datasource/CalendarsFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/datasource/CalendarsFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/datasource/CalendarsFilterAssemblerSubscription.kt index 4066159581..81c741b434 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/datasource/CalendarsFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/datasource/CalendarsFilterAssemblerSubscription.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.calendars.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt index e686b5cbfb..2ab24d626a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.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.calendars.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.detail import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt index 08e1939341..8ee6961fcd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt @@ -18,10 +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.calendars.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.detail -import android.content.Intent -import android.net.Uri import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -52,11 +50,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -91,6 +88,8 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_untitled import com.vitorpamplona.amethyst.commons.resources.edit_calendar_event import com.vitorpamplona.amethyst.commons.resources.route_calendar_event_detail import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage +import com.vitorpamplona.amethyst.commons.ui.components.rememberFileSharer +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture @@ -100,16 +99,15 @@ import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter import com.vitorpamplona.amethyst.commons.ui.note.types.CalendarRsvpRow import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatLongDate +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.rememberPhoneCalendar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size30dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.addToPhoneCalendar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.shareIcs import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.tags.people.PTag @@ -177,7 +175,9 @@ fun CalendarEventDetailScreen( } }, actions = { - val context = LocalContext.current + val phoneCalendar = rememberPhoneCalendar() + val fileSharer = rememberFileSharer() + val textSharer = rememberTextSharer() val icsChooserTitle = stringRes(Res.string.calendar_export_share_title) // Two share modes: // - .ics for non-nostr calendar apps (Google Calendar / iOS / Outlook) @@ -190,10 +190,10 @@ fun CalendarEventDetailScreen( // has no calendar app registered for ACTION_INSERT (rare; Wear OS, some // GrapheneOS profiles). IconButton(onClick = { - if (!addToPhoneCalendar(context, event)) { + if (!phoneCalendar.add(event)) { val ics = IcsExport.appointmentToIcs(event, targetAddress, TimeUtils.now()) val filename = IcsExport.appointmentFilename(event, targetAddress) - shareIcs(context, filename, ics, icsChooserTitle) + fileSharer.shareTextFile(filename, "text/calendar", ics, icsChooserTitle) } }) { Icon( @@ -206,7 +206,7 @@ fun CalendarEventDetailScreen( IconButton(onClick = { val ics = IcsExport.appointmentToIcs(event, targetAddress, TimeUtils.now()) val filename = IcsExport.appointmentFilename(event, targetAddress) - shareIcs(context, filename, ics, icsChooserTitle) + fileSharer.shareTextFile(filename, "text/calendar", ics, icsChooserTitle) }) { Icon( symbol = MaterialSymbols.Share, @@ -224,15 +224,7 @@ fun CalendarEventDetailScreen( targetAddress.dTag, null, ) - val intent = - Intent(Intent.ACTION_SEND) - .setType("text/plain") - .putExtra(Intent.EXTRA_TEXT, "nostr:$naddr") - context.startActivity( - Intent - .createChooser(intent, shareTitle) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), - ) + textSharer.share("nostr:$naddr", null, shareTitle) }) { Icon( symbol = MaterialSymbols.AutoMirrored.Send, @@ -347,7 +339,6 @@ private fun EventBody( fontWeight = FontWeight.Bold, ) } - val context = LocalContext.current val timeFormatter = rememberTimeOfDayFormatter() formatCalendarRange(note, timeFormatter)?.let { range -> Text( @@ -595,7 +586,7 @@ private fun HeroImage( @Composable private fun LocationRow(location: String) { - val context = LocalContext.current + val uriHandler = LocalUriHandler.current // NIP-52 `location` is free-text — a place name, an address, OR a meeting URL (Zoom, Jitsi, // a livestream link). Treat http(s) URLs as web links so they open in the browser; otherwise // hand to the maps app via the geo: intent. @@ -607,17 +598,10 @@ private fun LocationRow(location: String) { .clickable { runCatching { val trimmed = location.trim() - val intent = - if (isUrl) { - Intent(Intent.ACTION_VIEW, trimmed.toUri()) - } else { - // `geo:0,0?q=` is the Android geo intent; the user's - // installed maps app handles it. - Intent(Intent.ACTION_VIEW, "geo:0,0?q=${Uri.encode(trimmed)}".toUri()) - } - // runCatching swallows ActivityNotFoundException when no handler is + // `geo:0,0?q=` is the Android geo URI; the user's installed maps + // app handles it. runCatching swallows the failure when no handler is // installed — we don't have anywhere useful to fall back to. - context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + uriHandler.openUri(if (isUrl) trimmed else "geo:0,0?q=${encodeUriComponent(trimmed)}") } }.padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, @@ -905,3 +889,18 @@ private fun rememberCalendarsContaining(targetAddress: Address): State create(modelClass: Class): T = LiveStreamTopZappersViewModel(channel) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = LiveStreamTopZappersViewModel(channel) as T }, ) @@ -164,7 +169,7 @@ private fun ZapperPill( } ZapIcon(Size16Modifier, BitcoinOrange) Text( - text = showAmountInteger(BigDecimal.valueOf(entry.totalSats)), + text = showAmountInteger(BigDecimal(entry.totalSats)), style = MaterialTheme.typography.labelMedium, ) Spacer(Modifier.padding(start = 2.dp)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/AndroidChessAdapter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/AndroidChessAdapter.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/AndroidChessAdapter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/AndroidChessAdapter.kt index 5e39b89c07..326f554e62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/AndroidChessAdapter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/AndroidChessAdapter.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.chess +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessGameScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessGameScreen.kt index 11af042705..c55fc092a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessGameScreen.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.chess +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess -import androidx.activity.compose.LocalActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -56,8 +55,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.fragment.app.FragmentActivity -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.model.cache.LocalCache @@ -75,12 +72,13 @@ import com.vitorpamplona.amethyst.commons.resources.chess_game_waiting import com.vitorpamplona.amethyst.commons.resources.chess_loading_game import com.vitorpamplona.amethyst.commons.resources.go_back import com.vitorpamplona.amethyst.commons.resources.relay_settings +import com.vitorpamplona.amethyst.commons.ui.components.rememberWindowViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.datasource.ChessSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessSubscription import com.vitorpamplona.quartz.nip64Chess.ChessGameNameGenerator /** @@ -100,10 +98,8 @@ fun ChessGameScreen( nav: INav, ) { // Scope ViewModel to Activity so it's shared between lobby and game screens - val activity = LocalActivity.current as FragmentActivity val chessViewModel: ChessViewModel = - viewModel( - viewModelStoreOwner = activity, + rememberWindowViewModel( key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessLobbyScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessLobbyScreen.kt index 89e93c15d2..d4c9b5526d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessLobbyScreen.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.chess +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess -import androidx.activity.compose.LocalActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -56,8 +55,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.fragment.app.FragmentActivity -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.model.cache.LocalCache @@ -79,13 +76,14 @@ import com.vitorpamplona.amethyst.commons.resources.back import com.vitorpamplona.amethyst.commons.resources.dismiss import com.vitorpamplona.amethyst.commons.resources.relay_settings import com.vitorpamplona.amethyst.commons.resources.route_chess +import com.vitorpamplona.amethyst.commons.ui.components.rememberWindowViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.datasource.ChessSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessSubscription import com.vitorpamplona.quartz.nip64Chess.Color as ChessColor /** @@ -98,10 +96,8 @@ fun ChessLobbyScreen( nav: INav, ) { // Scope ViewModel to Activity so it's shared between lobby and game screens - val activity = LocalActivity.current as FragmentActivity val chessViewModel: ChessViewModel = - viewModel( - viewModelStoreOwner = activity, + rememberWindowViewModel( key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt index 16af1d0b1f..1f5e16e911 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.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.chess +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessViewModelFactory.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessViewModelFactory.kt index ab27e660cb..8472ec36d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessViewModelFactory.kt @@ -18,16 +18,16 @@ * 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.chess +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores -import com.vitorpamplona.amethyst.commons.nip64Chess.ChessDismissedGamesStore import com.vitorpamplona.amethyst.commons.nip64Chess.ChessPollingDefaults import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel +import com.vitorpamplona.amethyst.commons.nip64Chess.chessDismissedGamesStore import kotlin.reflect.KClass /** @@ -49,7 +49,7 @@ class ChessViewModelFactory( fetcher = AndroidRelayFetcher(account), metadataProvider = AndroidMetadataProvider(), pollingConfig = ChessPollingDefaults.android, - dismissedStorage = ChessDismissedGamesStore(chessDismissedGamesData(appStores)), + dismissedStorage = appStores.chessDismissedGamesStore(), ) as T } throw IllegalArgumentException("Unknown ViewModel class") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt index c5a072a0fc..d8bfb4390e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/datasource/ChessSubscription.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.chess.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt index 4a9cca8365..057755356d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.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.create +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.create -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -62,6 +59,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -70,7 +68,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign 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 @@ -99,7 +96,9 @@ import com.vitorpamplona.amethyst.commons.resources.geocache_owner_qr_warning import com.vitorpamplona.amethyst.commons.resources.geocache_photo_remove import com.vitorpamplona.amethyst.commons.resources.route_edit_geocache import com.vitorpamplona.amethyst.commons.resources.route_new_geocache +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage +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.geocacheEmoji @@ -139,7 +138,7 @@ fun NewGeocacheScreen( editPubKeyHex: String? = null, editDTag: String? = null, ) { - val model: NewGeocacheViewModel = viewModel() + val model: NewGeocacheViewModel = rememberViewModel { NewGeocacheViewModel() } val scope = rememberCoroutineScope() remember(editKind, editPubKeyHex, editDTag, prefillGeohash) { @@ -154,10 +153,13 @@ fun NewGeocacheScreen( var pickingLocation 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 rememberSaveable { 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 = { @@ -285,7 +287,7 @@ fun NewGeocacheScreen( Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { OutlinedButton( enabled = !model.isUploading.value, - onClick = { photoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, + onClick = { pickingPhoto = true }, ) { if (model.isUploading.value) { CircularProgressIndicator(Modifier.size(16.dp), strokeWidth = 2.dp) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt index 1c60602098..d869e1fd38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.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.create +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.create -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 @@ -175,7 +175,7 @@ class NewGeocacheViewModel : ViewModel() { fun isValid() = hasName() && hasLocation() 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/geocaches/detail/GeocacheAddToHuntSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt index 8ab7b184f9..7d98836520 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.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.geocaches.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt index 52e05596e9..be3783dfaf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.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.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail -import android.content.Intent import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -40,7 +39,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Size -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -57,6 +55,8 @@ import com.vitorpamplona.amethyst.commons.resources.geocache_log_type_dnf import com.vitorpamplona.amethyst.commons.resources.geocache_log_type_maintenance import com.vitorpamplona.amethyst.commons.resources.geocache_log_type_note import com.vitorpamplona.amethyst.commons.resources.geocache_verified_find +import com.vitorpamplona.amethyst.commons.resources.quick_action_share +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.GeocacheChip @@ -162,14 +162,11 @@ fun GeocacheWinnerStrip( /** Shares the cache as `nostr:naddr…`, which is how it opens in another NIP-CC client. */ @Composable fun GeocacheShareAction(address: Address) { - val context = LocalContext.current + val sharer = rememberTextSharer() + val shareStr = stringRes(Res.string.quick_action_share) IconButton(onClick = { val naddr = NAddress.create(address.kind, address.pubKeyHex, address.dTag, null) - val intent = - Intent(Intent.ACTION_SEND) - .setType("text/plain") - .putExtra(Intent.EXTRA_TEXT, "nostr:$naddr") - context.startActivity(Intent.createChooser(intent, null).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + sharer.share("nostr:$naddr", null, shareStr) }) { Icon( symbol = MaterialSymbols.AutoMirrored.Send, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt index d997133943..43e9902e2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.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.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail -import android.content.Intent import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -64,10 +63,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -427,12 +425,11 @@ private fun GeocacheNavigateRow( longitude: Double, name: String, ) { - val context = LocalContext.current + val uriHandler = LocalUriHandler.current OutlinedButton(onClick = { val label = name.ifBlank { "Geocache" } - val uri = "geo:$latitude,$longitude?q=$latitude,$longitude($label)".toUri() runCatching { - context.startActivity(Intent(Intent.ACTION_VIEW, uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + uriHandler.openUri("geo:$latitude,$longitude?q=$latitude,$longitude($label)") } }) { Icon( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheLogSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheLogSheet.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheLogSheet.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheLogSheet.kt index 87f4ab5b18..70f0b5c12d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheLogSheet.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheLogSheet.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.geocaches.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt index f69a4d5c89..ddbc21f310 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.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.geocaches.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/ShortNotePostScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 548500b82d..6f26fb38cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/ShortNotePostScreen.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.home +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween @@ -68,7 +68,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -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.model.BooleanType @@ -102,14 +101,18 @@ import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.commons.ui.actions.uploads.OnIncomingShare import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.UploadProgressIndicator +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField +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.PostingTopBar @@ -129,13 +132,17 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.MessageF import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.AudienceFlap import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.AudienceSheet import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.rememberAudienceLists +import com.vitorpamplona.amethyst.commons.ui.note.creators.polls.PollOptionsField import com.vitorpamplona.amethyst.commons.ui.note.creators.pow.PowOverrideButton import com.vitorpamplona.amethyst.commons.ui.note.creators.previews.DisplayPreviews import com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling.ScheduleAtButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling.ScheduleAtPicker +import com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling.roundUpToNextQuarterHour import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList +import com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls.ZapPollField import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraiserButton import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo @@ -161,14 +168,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow -import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator -import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection -import com.vitorpamplona.amethyst.ui.note.creators.polls.PollOptionsField -import com.vitorpamplona.amethyst.ui.note.creators.scheduling.ScheduleAtPicker -import com.vitorpamplona.amethyst.ui.note.creators.scheduling.roundUpToNextQuarterHour -import com.vitorpamplona.amethyst.ui.note.creators.zappolls.ZapPollField import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet @@ -189,7 +190,7 @@ fun ShortNotePostScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: ShortNotePostViewModel = viewModel() + val postViewModel: ShortNotePostViewModel = rememberViewModel { ShortNotePostViewModel() } postViewModel.init(accountViewModel) postViewModel.setGroupThread(groupThreadId, groupThreadRelayUrl) @@ -739,7 +740,7 @@ private fun NewPostScreenBody( showAlwaysOnPrompt = true } else { postViewModel.scheduledForSec = - roundUpToNextQuarterHour((System.currentTimeMillis() / 1000) + 60 * 60) + roundUpToNextQuarterHour(TimeUtils.now() + 60 * 60) } }, ) @@ -761,7 +762,7 @@ private fun NewPostScreenBody( TextButton(onClick = { showAlwaysOnPrompt = false postViewModel.scheduledForSec = - roundUpToNextQuarterHour((System.currentTimeMillis() / 1000) + 60 * 60) + roundUpToNextQuarterHour(TimeUtils.now() + 60 * 60) }) { Text(stringRes(Res.string.schedule_post_always_on_prompt_continue)) } @@ -779,7 +780,7 @@ private fun BottomRowActions( if (postViewModel.scheduledForSec != null) { null } else { - roundUpToNextQuarterHour((System.currentTimeMillis() / 1000) + 60 * 60) + roundUpToNextQuarterHour(TimeUtils.now() + 60 * 60) } }, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/VoiceReplyScreen.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/VoiceReplyScreen.kt index 7a37f4b475..edd2f1cecb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/VoiceReplyScreen.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.home +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -38,8 +38,11 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.ui.actions.mediaServers.FileServerSelectionRow +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.UploadProgressIndicator +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose @@ -49,9 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow -import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator -import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -64,7 +64,7 @@ fun VoiceReplyScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val viewModel: VoiceReplyViewModel = viewModel() + val viewModel: VoiceReplyViewModel = rememberViewModel { VoiceReplyViewModel() } LaunchedEffect(replyToNoteId, recordingFilePath, accountViewModel) { viewModel.init(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/VoiceReplyViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index fed2108c79..f10278ecad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/VoiceReplyViewModel.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.home +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -26,7 +26,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.audio.RecordingResult import com.vitorpamplona.amethyst.commons.audio.VoiceAnonymizationController import com.vitorpamplona.amethyst.commons.audio.VoicePreset @@ -58,6 +57,7 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -215,7 +215,6 @@ class VoiceReplyViewModel : ViewModel() { } catch (e: CancellationException) { Log.w("VoiceReplyViewModel", "User canceled, or ViewModel cleared", e) } catch (e: Exception) { - val appContext = Amethyst.instance.appContext accountViewModel.toastManager.toast( loadStringRes(Res.string.upload_error_title), loadStringRes( @@ -239,7 +238,6 @@ class VoiceReplyViewModel : ViewModel() { result: UploadingState, onSuccess: () -> Unit, ) { - val appContext = Amethyst.instance.appContext val uploadErrorTitle = loadStringRes(Res.string.upload_error_title) val uploadVoiceNip95NotSupported = loadStringRes(Res.string.upload_error_voice_message_nip95_not_supported) val uploadVoiceFailed = loadStringRes(Res.string.upload_error_voice_message_failed) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/polls/PollPostScreen.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/polls/PollPostScreen.kt index 2aa522582a..5d89b902a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/polls/PollPostScreen.kt @@ -18,18 +18,18 @@ * 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.polls +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.NewPostScreenInner import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.NewPostScreenInner import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.coroutines.FlowPreview @@ -41,7 +41,7 @@ fun PollPostScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: ShortNotePostViewModel = viewModel() + val postViewModel: ShortNotePostViewModel = rememberViewModel { ShortNotePostViewModel() } postViewModel.init(accountViewModel) LaunchedEffect(postViewModel, accountViewModel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt index 934fc14e28..fe3e644ab9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/profile/gallery/GalleryThumb.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.profile.gallery +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery -import androidx.annotation.OptIn import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -35,7 +34,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.media3.common.util.UnstableApi import coil3.compose.AsyncImagePainter import coil3.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImageContent @@ -61,8 +59,8 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier +import com.vitorpamplona.amethyst.commons.video.isHlsMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent @@ -216,7 +214,6 @@ fun DisplayGalleryAuthorBanner( } } -@OptIn(UnstableApi::class) @Composable fun GalleryContentView( contentList: List, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt index 34b9228931..a69dce7b8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/ComposeSettingsScreen.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.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -39,7 +39,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -109,7 +108,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory import kotlinx.coroutines.flow.MutableStateFlow import org.jetbrains.compose.resources.StringResource @@ -153,7 +151,7 @@ fun ComposeSettingsContent( ) SettingsDivider() // The F-Droid build ships no on-device model, so the toggle would do nothing there. - if (WritingAssistantFactory.IS_SUPPORTED) { + if (accountViewModel.host.supportsWritingAssistant) { BooleanSwitchTile( flow = sharedPrefs.automaticallyProposeAiImprovements, icon = MaterialSymbols.AutoAwesome, @@ -246,7 +244,6 @@ private fun PowDifficultyTile(accountViewModel: AccountViewModel) { private fun PowTimeEstimate(difficulty: Int) { if (difficulty <= 0) return - val context = LocalContext.current val estimate by produceState(initialValue = null, difficulty) { val rate = deviceHashesPerSecond() diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.ios.kt index 34b5b9ff77..433237d050 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.ios.kt +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.ios.kt @@ -22,7 +22,20 @@ package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.runtime.Composable -private val NoFileSharer = FileSharer { _, _, _ -> } +private object NoFileSharer : FileSharer { + override fun shareTextFilesAsZip( + zipName: String, + files: List>, + title: String, + ) {} + + override fun shareTextFile( + fileName: String, + mimeType: String, + content: String, + title: String, + ) {} +} /** No share sheet wired yet. */ @Composable diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.ios.kt new file mode 100644 index 0000000000..0cbe7794ae --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.ios.kt @@ -0,0 +1,29 @@ +/* + * 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.screen.loggedIn.calendars + +import androidx.compose.runtime.Composable + +private val NoPhoneCalendar = PhoneCalendar { false } + +/** No calendar composer wired yet: callers fall back to the .ics share. */ +@Composable +actual fun rememberPhoneCalendar(): PhoneCalendar = NoPhoneCalendar diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.ios.kt new file mode 100644 index 0000000000..232d530b46 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.ios.kt @@ -0,0 +1,46 @@ +/* + * 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.screen.loggedIn.calendars + +import platform.Foundation.NSDate +import platform.Foundation.NSRelativeDateTimeFormatter +import platform.Foundation.NSRelativeDateTimeFormatterUnitsStyleAbbreviated +import platform.Foundation.dateWithTimeIntervalSince1970 + +actual suspend fun relativeTimeSpan( + targetSeconds: Long, + nowSeconds: Long, + dayResolution: Boolean, +): String { + val formatter = NSRelativeDateTimeFormatter() + formatter.unitsStyle = NSRelativeDateTimeFormatterUnitsStyleAbbreviated + val day = 86_400L + val (target, now) = + if (dayResolution) { + Pair(targetSeconds.floorDiv(day) * day, nowSeconds.floorDiv(day) * day) + } else { + Pair(targetSeconds, nowSeconds) + } + return formatter.localizedStringForDate( + NSDate.dateWithTimeIntervalSince1970(target.toDouble()), + relativeToDate = NSDate.dateWithTimeIntervalSince1970(now.toDouble()), + ) +} diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.jvm.kt index 34b5b9ff77..433237d050 100644 --- a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.jvm.kt +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.jvm.kt @@ -22,7 +22,20 @@ package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.runtime.Composable -private val NoFileSharer = FileSharer { _, _, _ -> } +private object NoFileSharer : FileSharer { + override fun shareTextFilesAsZip( + zipName: String, + files: List>, + title: String, + ) {} + + override fun shareTextFile( + fileName: String, + mimeType: String, + content: String, + title: String, + ) {} +} /** No share sheet wired yet. */ @Composable diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.jvm.kt new file mode 100644 index 0000000000..0cbe7794ae --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.jvm.kt @@ -0,0 +1,29 @@ +/* + * 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.screen.loggedIn.calendars + +import androidx.compose.runtime.Composable + +private val NoPhoneCalendar = PhoneCalendar { false } + +/** No calendar composer wired yet: callers fall back to the .ics share. */ +@Composable +actual fun rememberPhoneCalendar(): PhoneCalendar = NoPhoneCalendar diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.jvm.kt new file mode 100644 index 0000000000..285118e8f4 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.jvm.kt @@ -0,0 +1,48 @@ +/* + * 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.screen.loggedIn.calendars + +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.d +import com.vitorpamplona.amethyst.commons.resources.h +import com.vitorpamplona.amethyst.commons.resources.m +import com.vitorpamplona.amethyst.commons.resources.now +import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.math.abs + +/** The JDK has no relative-time formatter: a compact signed span ("+2h", "-5m"). */ +actual suspend fun relativeTimeSpan( + targetSeconds: Long, + nowSeconds: Long, + dayResolution: Boolean, +): String { + val diff = targetSeconds - nowSeconds + val span = abs(diff) + val sign = if (diff < 0) "-" else "+" + return when { + span >= TimeUtils.ONE_DAY -> sign + (span / TimeUtils.ONE_DAY) + loadStringRes(Res.string.d) + dayResolution -> loadStringRes(Res.string.now) + span >= TimeUtils.ONE_HOUR -> sign + (span / TimeUtils.ONE_HOUR) + loadStringRes(Res.string.h) + span >= TimeUtils.ONE_MINUTE -> sign + (span / TimeUtils.ONE_MINUTE) + loadStringRes(Res.string.m) + else -> loadStringRes(Res.string.now) + } +} diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCardTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/SharedNoteCardTest.kt similarity index 99% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCardTest.kt rename to commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/SharedNoteCardTest.kt index 7d76a5852f..063ed1de85 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCardTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/SharedNoteCardTest.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.note.share +package com.vitorpamplona.amethyst.commons.ui.note.share import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/CordnCoordinatorNameTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/cordnGroup/CordnCoordinatorNameTest.kt similarity index 95% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/CordnCoordinatorNameTest.kt rename to commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/cordnGroup/CordnCoordinatorNameTest.kt index 339fa80822..a540b1387a 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/CordnCoordinatorNameTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/cordnGroup/CordnCoordinatorNameTest.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 +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.disambiguate import org.junit.Assert.assertEquals import org.junit.Test