├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.yml └── workflows │ ├── build-android.yml │ ├── build-linux.yml │ ├── build-windows.yml │ └── check_waiting_issue.yml ├── .gitignore ├── .gitmodules ├── .idea └── icon.svg ├── LICENSE ├── OrganiseStrings.py ├── README.md ├── ReplaceStrings.py ├── androidApp ├── .gitignore ├── build.gradle.kts ├── debug.keystore ├── keystore.properties.debug ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── com │ │ └── toasterofbread │ │ └── spmp │ │ ├── MainActivity.kt │ │ └── widget │ │ ├── LyricsLineHorizontalWidgetReceiver.kt │ │ ├── SongQueueWidgetReceiver.kt │ │ ├── SpMpWidgetReceiver.kt │ │ ├── SplitImageControlsWidgetReceiver.kt │ │ ├── WidgetConfigurationActivity.kt │ │ └── WidgetTypeMapper.kt │ └── res │ ├── drawable-ja │ └── widget_preview_song_queue.png │ ├── drawable │ ├── ic_launcher_background.xml │ ├── ic_launcher_monochrome.xml │ ├── ic_spmp.xml │ ├── widget_preview_image_and_controls.png │ ├── widget_preview_lyrics_line_horizontal.png │ └── widget_preview_song_queue.png │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── values-ja │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ ├── data_extraction_rules.xml │ ├── lyrics_line_horizontal_widget_provider.xml │ ├── song_queue_widget_provider.xml │ └── split_image_controls_widget_provider.xml ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── project.properties ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── plugins │ ├── generate-build-config.gradle.kts │ ├── generate-dependency-list.gradle.kts │ ├── shared │ ├── Command.kt │ └── DesktopUtils.kt │ └── spmp │ ├── Dependencies.kt │ ├── DependencyInfo.kt │ └── ProjectConfigValues.kt ├── desktopApp ├── appimage │ ├── AppRun │ └── spmp.desktop ├── build.gradle.kts └── src │ └── jvmMain │ └── kotlin │ ├── ErrorDialog.kt │ └── main.kt ├── docker-image ├── Dockerfile ├── buildAllAndroid.sh ├── buildAllDesktop.sh ├── dockerBuild.sh └── gradleEntryPoint.sh ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── metadata ├── dev.toastbits.spmp.metainfo.xml └── en-US │ ├── full_description.txt │ ├── images │ ├── icon.ico │ ├── icon.png │ ├── icon.svg │ ├── icon_round.png │ ├── icon_simple.svg │ └── phoneScreenshots │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ ├── 9.png │ │ ├── landscape_0.png │ │ ├── landscape_1.png │ │ └── landscape_2.png │ ├── short_description.txt │ └── title.txt ├── readme └── README-ja.md ├── settings.gradle.kts └── shared ├── build.gradle.kts └── src ├── androidMain ├── kotlin │ ├── SpMp.android.kt │ └── com │ │ └── toasterofbread │ │ └── spmp │ │ ├── ErrorReportActivity.kt │ │ ├── model │ │ └── appaction │ │ │ └── shortcut │ │ │ └── DefaultShortcuts.android.kt │ │ ├── platform │ │ ├── AppContext.android.kt │ │ ├── DiscordStatus.android.kt │ │ ├── ImageBitmap.android.kt │ │ ├── PlatformService.android.kt │ │ ├── PlayerListener.android.kt │ │ ├── SqlDriver.android.kt │ │ ├── VideoPlayback.android.kt │ │ ├── WebViewLogin.android.kt │ │ ├── download │ │ │ ├── LocalSongMetadataProcessor.android.kt │ │ │ ├── PlayerDownloadManager.android.kt │ │ │ └── PlayerDownloadService.kt │ │ ├── getMediaDataSpecPlaybackUri.kt │ │ ├── playerservice │ │ │ ├── AudioDeviceCallback.kt │ │ │ ├── ExoPlayerUtils.kt │ │ │ ├── ForegroundPlayerService.kt │ │ │ ├── ForegroundPlayerServicePlayerListener.kt │ │ │ ├── HeadlessExternalPlayerService.kt │ │ │ ├── InternalPlayerServiceCompanion.kt │ │ │ ├── LocalServer.android.kt │ │ │ ├── MediaDataSpecProcessor.kt │ │ │ ├── NotificationImageUtils.kt │ │ │ ├── PlatformExternalPlayerService.android.kt │ │ │ ├── PlatformInternalPlayerService.android.kt │ │ │ ├── PlayerServiceNotificationCustomAction.kt │ │ │ ├── PlayerSessionCallback.kt │ │ │ ├── SpMs.android.kt │ │ │ ├── createDataSourceFactory.kt │ │ │ ├── initialiseSessionAndPlayer.kt │ │ │ └── notification │ │ │ │ ├── NotificationState.kt │ │ │ │ ├── NotificationStateManager.kt │ │ │ │ └── PlayerServiceNotificationManager.kt │ │ └── visualiser │ │ │ ├── FFTAudioProcessor.kt │ │ │ ├── LICENSE │ │ │ └── MusicVisualiser.android.kt │ │ ├── ui │ │ ├── SongLikedStatusIcon.kt │ │ └── layout │ │ │ └── nowplaying │ │ │ └── overlay │ │ │ └── NotifImageOverlayMenu.android.kt │ │ └── widget │ │ ├── SpMpWidget.kt │ │ ├── SpMpWidgetUpdater.kt │ │ ├── WidgetActionCallback.kt │ │ ├── WidgetUpdateListener.kt │ │ ├── action │ │ ├── CommonAction.kt │ │ ├── PlayPauseAction.kt │ │ └── QueueSeekAction.kt │ │ ├── component │ │ ├── GlanceActionButton.kt │ │ ├── GlanceActionButtonGrid.kt │ │ ├── GlanceCanvas.kt │ │ ├── GlanceLargePlayPauseButton.kt │ │ ├── GlanceLazyColumn.kt │ │ ├── GlanceText.kt │ │ ├── spmp │ │ │ ├── GlanceSongPreview.kt │ │ │ └── GlanceSongThumbnail.kt │ │ └── styledcolumn │ │ │ └── GlanceStyledColumn.kt │ │ ├── impl │ │ ├── BasicControlsWidget.kt │ │ ├── LyricsLineHorizontalWidget.kt │ │ ├── LyricsWidget.kt │ │ ├── SongQueueWidget.kt │ │ └── SplitImageControlsWidget.kt │ │ ├── mapper │ │ └── FontResourceMapper.kt │ │ └── modifier │ │ ├── padding.kt │ │ ├── size.kt │ │ └── systemCornerRadius.kt └── res │ └── drawable │ ├── ic_heart.xml │ ├── ic_heart_off.xml │ ├── ic_pause.xml │ ├── ic_play.xml │ ├── ic_settings.xml │ ├── ic_skip_next.xml │ ├── ic_skip_previous.xml │ ├── ic_spmp.xml │ ├── ic_spmp_old.xml │ ├── ic_thumb_down.xml │ ├── ic_thumb_up.xml │ ├── ic_thumb_up_off.xml │ └── ic_visibility.xml ├── commonMain ├── composeResources │ ├── drawable │ │ ├── ic_discord.xml │ │ ├── ic_github.xml │ │ ├── ic_splash.png │ │ └── ic_spmp.png │ ├── values-en-rGB │ │ └── strings.xml │ ├── values-en-rUS │ │ └── strings.xml │ ├── values-es-rUS │ │ └── strings.xml │ ├── values-fr-rFR │ │ └── strings.xml │ ├── values-ja-rJP │ │ └── strings.xml │ ├── values-pl-rPL │ │ └── strings.xml │ ├── values-ru-rRU │ │ └── strings.xml │ ├── values-tr-rTR │ │ └── strings.xml │ ├── values-zh-rCN │ │ └── strings.xml │ ├── values-zh-rTW │ │ └── strings.xml │ └── values │ │ └── strings.xml ├── kotlin │ ├── Coroutines.kt │ ├── ProgramArguments.kt │ ├── SpMp.kt │ └── com │ │ └── toasterofbread │ │ └── spmp │ │ ├── model │ │ ├── JsonHttpClient.kt │ │ ├── MediaItemLayoutParams.kt │ │ ├── SongFeedFilterChip.kt │ │ ├── UiString.kt │ │ ├── appaction │ │ │ ├── AppAction.kt │ │ │ ├── NavigationAppAction.kt │ │ │ ├── OtherAppAction.kt │ │ │ ├── PlaybackAppAction.kt │ │ │ ├── SongAppAction.kt │ │ │ ├── action │ │ │ │ ├── navigation │ │ │ │ │ ├── AppPageNavigationAction.kt │ │ │ │ │ ├── JumpToLyricsNavigationAction.kt │ │ │ │ │ ├── NavigationAction.kt │ │ │ │ │ └── TogglePlayerNavigationAction.kt │ │ │ │ └── playback │ │ │ │ │ ├── PlayPausePlaybackActions.kt │ │ │ │ │ ├── PlaybackAction.kt │ │ │ │ │ ├── QueuePlaybackActions.kt │ │ │ │ │ └── SeekPlaybackActions.kt │ │ │ └── shortcut │ │ │ │ ├── DefaultShortcuts.kt │ │ │ │ ├── Shortcut.kt │ │ │ │ ├── ShortcutIndicator.kt │ │ │ │ ├── ShortcutState.kt │ │ │ │ ├── ShortcutTriggerSelector.kt │ │ │ │ ├── ShortcutsEditor.kt │ │ │ │ └── trigger │ │ │ │ ├── KeyboardShortcutTrigger.kt │ │ │ │ ├── MouseButtonShortcutTrigger.kt │ │ │ │ └── ShortcutTrigger.kt │ │ ├── lyrics │ │ │ ├── LyricsFileConverter.kt │ │ │ ├── SongLyrics.kt │ │ │ └── loadLyricsOnSongChange.kt │ │ ├── mediaitem │ │ │ ├── MediaItem.kt │ │ │ ├── MediaItemData.kt │ │ │ ├── MediaItemPreviewInteraction.kt │ │ │ ├── MediaItemSortType.kt │ │ │ ├── MediaitemHolder.kt │ │ │ ├── PropertyRememberer.kt │ │ │ ├── ToInfoString.kt │ │ │ ├── Uid.kt │ │ │ ├── UnsupportedPropertyRememberer.kt │ │ │ ├── artist │ │ │ │ ├── Artist.kt │ │ │ │ ├── ArtistData.kt │ │ │ │ ├── ArtistLayout.kt │ │ │ │ ├── Subscribed.kt │ │ │ │ ├── SubscriberCount.kt │ │ │ │ └── formatArtistTitles.kt │ │ │ ├── db │ │ │ │ ├── Delete.kt │ │ │ │ ├── HiddenItems.kt │ │ │ │ ├── LikedSongs.kt │ │ │ │ ├── ObserveAsState.kt │ │ │ │ ├── PinnedItems.kt │ │ │ │ ├── PlayCount.kt │ │ │ │ ├── QueryProperty.kt │ │ │ │ ├── SongFeedCache.kt │ │ │ │ ├── ThemeColour.kt │ │ │ │ └── Utils.kt │ │ │ ├── enums │ │ │ │ ├── MediaItemType.kt │ │ │ │ └── PlaylistType.kt │ │ │ ├── layout │ │ │ │ ├── AppMediaItemLayout.kt │ │ │ │ ├── ContinuableMediaItemLayout.kt │ │ │ │ └── YoutubePageType.kt │ │ │ ├── library │ │ │ │ ├── LocalPlaylists.kt │ │ │ │ ├── LocalSongSyncLoader.kt │ │ │ │ ├── MediaItemLibrary.kt │ │ │ │ └── SyncLoader.kt │ │ │ ├── loader │ │ │ │ ├── ArtistSubscribedLoader.kt │ │ │ │ ├── Loader.kt │ │ │ │ ├── MediaItemLoader.kt │ │ │ │ ├── MediaItemThumbnailLoader.kt │ │ │ │ ├── SongLikedLoader.kt │ │ │ │ └── SongLyricsLoader.kt │ │ │ ├── playlist │ │ │ │ ├── InteractivePlaylistEditor.kt │ │ │ │ ├── LocalPlaylist.kt │ │ │ │ ├── LocalPlaylistData.kt │ │ │ │ ├── LocalPlaylistEditor.kt │ │ │ │ ├── LocalPlaylistRef.kt │ │ │ │ ├── OwnedPlaylists.kt │ │ │ │ ├── Playlist.kt │ │ │ │ ├── PlaylistData.kt │ │ │ │ ├── PlaylistFileConverter.kt │ │ │ │ ├── PlaylistHolder.kt │ │ │ │ ├── RemotePlaylist.kt │ │ │ │ ├── RemotePlaylistData.kt │ │ │ │ └── RemotePlaylistRef.kt │ │ │ ├── song │ │ │ │ ├── Song.kt │ │ │ │ ├── SongAudioQuality.kt │ │ │ │ ├── SongData.kt │ │ │ │ ├── SongLikedStatus.kt │ │ │ │ └── SongRef.kt │ │ │ └── toThumbnailProvider.kt │ │ ├── radio │ │ │ ├── PlaylistItemsRadioContinuation.kt │ │ │ ├── RadioContinuationSerializer.kt │ │ │ ├── RadioInstance.kt │ │ │ └── RadioState.kt │ │ └── settings │ │ │ ├── PackUserAuthState.kt │ │ │ ├── Settings.kt │ │ │ ├── SettingsGroup.kt │ │ │ ├── SettingsGroupImpl.kt │ │ │ └── category │ │ │ ├── BehaviourSettings.kt │ │ │ ├── DependencySettings.kt │ │ │ ├── DiscordAuthSettings.kt │ │ │ ├── DiscordSettings.kt │ │ │ ├── ExperimentalSettings.kt │ │ │ ├── FeedSettings.kt │ │ │ ├── FilterSettings.kt │ │ │ ├── InterfaceSettings.kt │ │ │ ├── LayoutSettings.kt │ │ │ ├── LyricsSettings.kt │ │ │ ├── MiscSettings.kt │ │ │ ├── PlatformSettings.kt │ │ │ ├── PlayerSettings.kt │ │ │ ├── SearchSettings.kt │ │ │ ├── ShortcutSettings.kt │ │ │ ├── StreamingSettings.kt │ │ │ ├── ThemeSettings.kt │ │ │ ├── WidgetSettings.kt │ │ │ ├── YTApiSettings.kt │ │ │ └── YoutubeAuthSettings.kt │ │ ├── platform │ │ ├── AppContext.kt │ │ ├── DiscordStatus.kt │ │ ├── FormFactor.kt │ │ ├── ImageBitmap.kt │ │ ├── PlatformService.kt │ │ ├── PlayerListener.kt │ │ ├── ProjectJson.kt │ │ ├── SqlDriver.kt │ │ ├── VideoPlayback.kt │ │ ├── WebViewLogin.kt │ │ ├── download │ │ │ ├── DownloadMethodSelectionDialog.kt │ │ │ └── PlayerDownloadManager.kt │ │ ├── playerservice │ │ │ ├── ClientServerPlayerService.kt │ │ │ ├── ExternalPlayerService.kt │ │ │ ├── ForwardingPlayerService.kt │ │ │ ├── LocalServer.kt │ │ │ ├── PlatformExternalPlayerService.kt │ │ │ ├── PlatformInternalPlayerService.kt │ │ │ ├── PlayerService.kt │ │ │ ├── PlayerServiceCompanion.kt │ │ │ ├── PlayerServicePlayer.kt │ │ │ ├── SpMs.kt │ │ │ ├── SpMsPlayerService.kt │ │ │ ├── UndoHandler.kt │ │ │ ├── applyPlayerEvents.kt │ │ │ ├── applyServerState.kt │ │ │ └── tryConnectToServer.kt │ │ └── visualiser │ │ │ └── MusicVisualiser.kt │ │ ├── resources │ │ ├── Resources.kt │ │ └── migrations │ │ │ ├── 1.kt │ │ │ ├── 2.kt │ │ │ ├── 5.kt │ │ │ ├── 6.kt │ │ │ ├── 7.kt │ │ │ ├── 8.kt │ │ │ ├── 9.kt │ │ │ └── Migration.kt │ │ ├── service │ │ └── playercontroller │ │ │ ├── DiscordStatusHandler.kt │ │ │ ├── PersistentQueueHandler.kt │ │ │ ├── PlayerClickOverrides.kt │ │ │ ├── PlayerState.kt │ │ │ ├── PlayerStatus.kt │ │ │ ├── RadioHandler.kt │ │ │ └── openUri.kt │ │ ├── ui │ │ ├── component │ │ │ ├── ColourSelectionDialog.kt │ │ │ ├── EndpointNotImplementedMessage.kt │ │ │ ├── ErrorInfoDisplay.kt │ │ │ ├── HorizontalFuriganaText.kt │ │ │ ├── LargeFilterList.kt │ │ │ ├── LikeDislikeButton.kt │ │ │ ├── LyricsLineDisplay.kt │ │ │ ├── MediaItemThumbnail.kt │ │ │ ├── MediaItemTitleEditDialog.kt │ │ │ ├── PillMenu.kt │ │ │ ├── PinnedItemsList.kt │ │ │ ├── VerticalFuriganaText.kt │ │ │ ├── WaveBorder.kt │ │ │ ├── longpressmenu │ │ │ │ ├── LongPressMenu.android.kt │ │ │ │ ├── LongPressMenu.desktop.kt │ │ │ │ ├── LongPressMenu.kt │ │ │ │ ├── LongPressMenuActionProvider.kt │ │ │ │ ├── LongPressMenuActions.kt │ │ │ │ ├── LongPressMenuContent.kt │ │ │ │ ├── LongPressMenuData.kt │ │ │ │ ├── artist │ │ │ │ │ ├── ArtistLongPressMenuActions.kt │ │ │ │ │ └── ArtistLongPressMenuInfo.kt │ │ │ │ ├── playlist │ │ │ │ │ ├── PlaylistLongPressMenuActions.kt │ │ │ │ │ └── PlaylistLongPressMenuInfo.kt │ │ │ │ └── song │ │ │ │ │ ├── SongLongPressMenuActions.kt │ │ │ │ │ └── SongLongPressMenuInfo.kt │ │ │ ├── mediaitemlayout │ │ │ │ ├── MediaItemCard.kt │ │ │ │ ├── MediaItemGrid.kt │ │ │ │ ├── MediaItemLayoutTitleBar.kt │ │ │ │ └── MediaItemList.kt │ │ │ ├── mediaitempreview │ │ │ │ ├── MediaItemPreview.kt │ │ │ │ └── ThumbShapes.kt │ │ │ ├── multiselect │ │ │ │ ├── AppPageMultiSelectContext.kt │ │ │ │ ├── MediaItemMultiSelectContext.kt │ │ │ │ ├── MultiSelectGeneralActions.kt │ │ │ │ ├── MultiSelectInfoDisplay.kt │ │ │ │ ├── MultiSelectNextRowActions.kt │ │ │ │ └── MultiSelectOverflowActions.kt │ │ │ ├── radio │ │ │ │ └── RadioStatusDisplay.kt │ │ │ └── shortcut │ │ │ │ └── ShortcutPreview.kt │ │ ├── layout │ │ │ ├── BarColourState.kt │ │ │ ├── DiscordLogin.kt │ │ │ ├── DiscordManualLogin.kt │ │ │ ├── GenericFeedViewMorePage.kt │ │ │ ├── ManualLoginPage.kt │ │ │ ├── PinnedItemsRow.kt │ │ │ ├── PlaylistSelectMenu.kt │ │ │ ├── ProjectInfoDialog.kt │ │ │ ├── SongRelatedPage.kt │ │ │ ├── apppage │ │ │ │ ├── AppPage.kt │ │ │ │ ├── AppPageState.kt │ │ │ │ ├── ControlPanelAppPage.kt │ │ │ │ ├── GenericFeedViewMoreAppPage.kt │ │ │ │ ├── RadioBuilderAppPage.kt │ │ │ │ ├── SongAppPage.kt │ │ │ │ ├── controlpanelpage │ │ │ │ │ ├── ControlPanelDownloadsPage.kt │ │ │ │ │ └── ControlPanelServerPage.kt │ │ │ │ ├── library │ │ │ │ │ ├── LibraryAlbumsPage.kt │ │ │ │ │ ├── LibraryAppPage.kt │ │ │ │ │ ├── LibraryArtistsPage.kt │ │ │ │ │ ├── LibraryPlaylistsPage.kt │ │ │ │ │ ├── LibraryProfilePage.kt │ │ │ │ │ ├── LibrarySongsPage.kt │ │ │ │ │ └── pageselector │ │ │ │ │ │ └── LibraryIconButtonPageSelector.kt │ │ │ │ ├── mainpage │ │ │ │ │ ├── MainPageDisplay.kt │ │ │ │ │ └── RootView.kt │ │ │ │ ├── searchpage │ │ │ │ │ ├── HorizontalSearchPagePrimaryBar.kt │ │ │ │ │ ├── HorizontalSearchPageSecondaryBar.kt │ │ │ │ │ ├── SearchAppPage.kt │ │ │ │ │ ├── SearchBar.kt │ │ │ │ │ ├── SearchFiltersRow.kt │ │ │ │ │ ├── SearchSettingsDialog.kt │ │ │ │ │ ├── SearchSuggestionsColumn.kt │ │ │ │ │ ├── SearchType.kt │ │ │ │ │ ├── VerticalSearchPagePrimaryBar.kt │ │ │ │ │ └── VerticalSearchPageSecondaryBar.kt │ │ │ │ ├── settingspage │ │ │ │ │ ├── AppSliderItem.kt │ │ │ │ │ ├── AppStringSetItem.kt │ │ │ │ │ ├── DiscordAuthItem.kt │ │ │ │ │ ├── DiscordLoginScreen.kt │ │ │ │ │ ├── ResetConfirmationDialog.kt │ │ │ │ │ ├── SettingsAppPage.kt │ │ │ │ │ ├── YoutubeMusicLoginScreen.kt │ │ │ │ │ ├── YtmAuthItem.kt │ │ │ │ │ └── category │ │ │ │ │ │ ├── BehaviourCategory.kt │ │ │ │ │ │ ├── DiscordCategory.kt │ │ │ │ │ │ ├── ExperimentalCategory.kt │ │ │ │ │ │ ├── FeedCategory.kt │ │ │ │ │ │ ├── FilterCategory.kt │ │ │ │ │ │ ├── LayoutCategory.kt │ │ │ │ │ │ ├── LyricsCategory.kt │ │ │ │ │ │ ├── MiscCategory.kt │ │ │ │ │ │ ├── PlatformCategory.kt │ │ │ │ │ │ ├── PlayerCategory.kt │ │ │ │ │ │ ├── SearchCategory.kt │ │ │ │ │ │ ├── ShortcutCategory.kt │ │ │ │ │ │ ├── StreamingCategory.kt │ │ │ │ │ │ ├── ThemeCategory.kt │ │ │ │ │ │ ├── WidgetCategory.kt │ │ │ │ │ │ └── YoutubeAccountCategory.kt │ │ │ │ └── songfeedpage │ │ │ │ │ ├── LFFSongFeedAppPage.kt │ │ │ │ │ ├── LFFSongFeedPagePrimaryBar.kt │ │ │ │ │ ├── SFFSongFeedAppPage.kt │ │ │ │ │ ├── SFFSongFeedPagePrimaryBar.kt │ │ │ │ │ ├── SongFeedAppPage.kt │ │ │ │ │ └── SongFeedPageLoadingView.kt │ │ │ ├── artistpage │ │ │ │ ├── ArtistActionBar.kt │ │ │ │ ├── ArtistAppPage.kt │ │ │ │ ├── ArtistLayout.kt │ │ │ │ ├── ArtistPageGetAllItems.kt │ │ │ │ ├── ArtistPageTitleBar.kt │ │ │ │ ├── DescriptionCard.kt │ │ │ │ ├── InfoDialog.kt │ │ │ │ ├── LocalArtistPage.kt │ │ │ │ ├── SFFArtistPage.kt │ │ │ │ ├── SubscribeButton.kt │ │ │ │ └── lff │ │ │ │ │ ├── LFFArtistPage.kt │ │ │ │ │ ├── LLFArtistPageEndPane.kt │ │ │ │ │ └── LLFArtistPageStartPane.kt │ │ │ ├── contentbar │ │ │ │ ├── CircularReferenceWarning.kt │ │ │ │ ├── ContentBar.kt │ │ │ │ ├── ContentBarElementSelector.kt │ │ │ │ ├── ContentBarReference.kt │ │ │ │ ├── ContentBarSelector.kt │ │ │ │ ├── CustomContentBar.kt │ │ │ │ ├── CustomContentBarCopyPasteButtons.kt │ │ │ │ ├── CustomContentBarEditor.kt │ │ │ │ ├── CustomContentBarTemplate.kt │ │ │ │ ├── InternalContentBar.kt │ │ │ │ ├── TemplateCustomContentBar.kt │ │ │ │ ├── element │ │ │ │ │ ├── ContentBarElement.kt │ │ │ │ │ ├── ContentBarElementButton.kt │ │ │ │ │ ├── ContentBarElementContentBar.kt │ │ │ │ │ ├── ContentBarElementCrossfade.kt │ │ │ │ │ ├── ContentBarElementLyrics.kt │ │ │ │ │ ├── ContentBarElementPinnedItems.kt │ │ │ │ │ ├── ContentBarElementSpacer.kt │ │ │ │ │ └── ContentBarElementVisualiser.kt │ │ │ │ └── layoutslot │ │ │ │ │ ├── ColourSource.kt │ │ │ │ │ ├── LandscapeLayoutSlot.kt │ │ │ │ │ ├── LayoutSlot.kt │ │ │ │ │ ├── LayoutSlotEditor.kt │ │ │ │ │ ├── LayoutSlotEditorPreviewOptionsList.kt │ │ │ │ │ └── PortraitLayoutSlot.kt │ │ │ ├── loadingsplash │ │ │ │ ├── ExtraLoadingContent.kt │ │ │ │ └── LoadingSplash.kt │ │ │ ├── nowplaying │ │ │ │ ├── NowPlaying.kt │ │ │ │ ├── NowPlayingPage.kt │ │ │ │ ├── NowPlayingTopBar.kt │ │ │ │ ├── NowPlayingTopOffsetSection.kt │ │ │ │ ├── PlayerExpansionState.kt │ │ │ │ ├── container │ │ │ │ │ ├── MinimisedProgressBar.kt │ │ │ │ │ ├── NowPlayingContainer.kt │ │ │ │ │ ├── PlayerBackground.kt │ │ │ │ │ ├── PlayerOverscroll.kt │ │ │ │ │ ├── ThumbnailBackground.kt │ │ │ │ │ ├── UpdateAnchors.kt │ │ │ │ │ ├── UpdateBarColours.kt │ │ │ │ │ └── VideoBackground.kt │ │ │ │ ├── maintab │ │ │ │ │ ├── Controls.kt │ │ │ │ │ ├── LargeBottomBar.kt │ │ │ │ │ ├── NowPlayingMainTabActionButtons.kt │ │ │ │ │ ├── NowPlayingMainTabLarge.kt │ │ │ │ │ ├── NowPlayingMainTabNarrow.kt │ │ │ │ │ ├── NowPlayingMainTabPage.kt │ │ │ │ │ ├── NowPlayingMainTabPortrait.kt │ │ │ │ │ ├── SeekBar.kt │ │ │ │ │ ├── VolumeSlider.kt │ │ │ │ │ └── thumbnailrow │ │ │ │ │ │ ├── ControlButtons.kt │ │ │ │ │ │ ├── LargeThumbnailRow.kt │ │ │ │ │ │ └── SmallThumbnailRow.kt │ │ │ │ ├── overlay │ │ │ │ │ ├── MainOverlayMenu.kt │ │ │ │ │ ├── NotifImageOverlayMenu.kt │ │ │ │ │ ├── PlayerOverlayMenu.kt │ │ │ │ │ ├── RelatedContentOverlayMenu.kt │ │ │ │ │ ├── SongThemeOverlayMenu.kt │ │ │ │ │ ├── lyrics │ │ │ │ │ │ ├── CoreLyricsDisplay.kt │ │ │ │ │ │ ├── LyricsOverlayMenu.kt │ │ │ │ │ │ ├── LyricsSearchMenu.kt │ │ │ │ │ │ ├── LyricsSearchResults.kt │ │ │ │ │ │ ├── LyricsSyncMenu.kt │ │ │ │ │ │ ├── SpecialMode.kt │ │ │ │ │ │ └── Terms.kt │ │ │ │ │ └── songtheme │ │ │ │ │ │ ├── DropdownOption.kt │ │ │ │ │ │ ├── SliderOption.kt │ │ │ │ │ │ ├── SongThemeOption.kt │ │ │ │ │ │ └── SongThemeOverlayMenu.kt │ │ │ │ └── queue │ │ │ │ │ ├── CurrentRadioIndicator.kt │ │ │ │ │ ├── NowPlayingQueuePage.kt │ │ │ │ │ ├── QueueButtonsRow.kt │ │ │ │ │ ├── QueueItems.kt │ │ │ │ │ ├── QueueTab.kt │ │ │ │ │ ├── QueueTabItem.kt │ │ │ │ │ ├── RepeatButton.kt │ │ │ │ │ └── StopAfterSongButton.kt │ │ │ ├── playlistpage │ │ │ │ ├── PlaylistAppPage.kt │ │ │ │ ├── PlaylistButtonBar.kt │ │ │ │ ├── PlaylistFooter.kt │ │ │ │ ├── PlaylistInteractionBar.kt │ │ │ │ ├── PlaylistItems.kt │ │ │ │ ├── PlaylistTopInfo.kt │ │ │ │ ├── ThumbnailSelectionDialog.kt │ │ │ │ └── TopInfoEditButtons.kt │ │ │ ├── radiobuilder │ │ │ │ ├── FilterSelectionPage.kt │ │ │ │ ├── RadioArtistSelector.kt │ │ │ │ ├── RadioBuilderIcon.kt │ │ │ │ ├── RadioBuilderPage.kt │ │ │ │ ├── RadioFilters.kt │ │ │ │ └── RecordArc.kt │ │ │ └── youtubemusiclogin │ │ │ │ ├── AccountSelectionPage.kt │ │ │ │ ├── LoginPage.kt │ │ │ │ ├── YoutubeMusicLoginPage.kt │ │ │ │ ├── YoutubeMusicManualLogin.kt │ │ │ │ └── YoutubeMusicWebviewLogin.kt │ │ ├── theme │ │ │ └── ApplicationTheme.kt │ │ └── util │ │ │ ├── LyricsLineState.kt │ │ │ └── WaveShape.kt │ │ ├── util │ │ ├── ListUtils.kt │ │ └── SongLikedStatusToggleTarget.kt │ │ ├── widget │ │ ├── SpMpWidgetType.kt │ │ ├── action │ │ │ ├── LyricsWidgetClickAction.kt │ │ │ ├── SongImageWidgetClickAction.kt │ │ │ ├── SongQueueWidgetClickAction.kt │ │ │ ├── SplitImageControlsWidgetClickAction.kt │ │ │ ├── TypeWidgetClickAction.kt │ │ │ └── WidgetClickAction.kt │ │ └── configuration │ │ │ ├── SpMpWidgetConfiguration.kt │ │ │ ├── WidgetConfig.kt │ │ │ ├── base │ │ │ ├── BaseWidgetConfig.kt │ │ │ └── BaseWidgetConfigDefaultsMask.kt │ │ │ ├── enum │ │ │ ├── WidgetSectionTheme.kt │ │ │ └── WidgetStyledBorderMode.kt │ │ │ ├── type │ │ │ ├── LyricsWidgetConfig.kt │ │ │ ├── LyricsWidgetConfigDefaultsMask.kt │ │ │ ├── SongImageWidgetConfig.kt │ │ │ ├── SongImageWidgetConfigDefaultsMask.kt │ │ │ ├── SongQueueWidgetConfig.kt │ │ │ ├── SongQueueWidgetConfigDefaultsMask.kt │ │ │ ├── SplitImageControlsWidgetConfig.kt │ │ │ ├── SplitImageControlsWidgetConfigDefaultsMask.kt │ │ │ ├── TypeConfigurationDefaultsMask.kt │ │ │ └── TypeWidgetConfig.kt │ │ │ └── ui │ │ │ └── screen │ │ │ └── WidgetConfigurationScreen.kt │ │ └── youtubeapi │ │ ├── AccountSwitcherEndpoint.kt │ │ ├── SpMpMediaItemCache.kt │ │ ├── SpMpYoutubeiApi.kt │ │ ├── SpMpYoutubeiAuthenticationState.kt │ │ ├── YoutubeMusicLogin.kt │ │ ├── YtmApiType.kt │ │ └── lyrics │ │ ├── Furigana.kt │ │ ├── Kugou.kt │ │ ├── Lrclib.kt │ │ ├── Lyrics.kt │ │ ├── Petit.kt │ │ ├── YoutubeMusic.kt │ │ ├── kugou │ │ ├── LoadKugouLyrics.kt │ │ └── SearchKugouLyrics.kt │ │ ├── lrclib │ │ ├── LoadLrclibLyrics.kt │ │ └── SearchLrclibLyrics.kt │ │ └── petit │ │ ├── ParsePetitLyrics.kt │ │ └── SearchPetitLyrics.kt ├── resources │ └── compose-multiplatform.xml └── sqldelight │ └── com │ └── toasterofbread │ └── spmp │ └── db │ ├── Version.sq │ ├── mediaitem │ ├── Artist.sq │ ├── ArtistLayout.sq │ ├── ArtistLayoutItem.sq │ ├── MediaItem.sq │ ├── MediaItemPlayCount.sq │ ├── PinnedItem.sq │ ├── Playlist.sq │ ├── PlaylistItem.sq │ └── Song.sq │ ├── persistentqueue │ ├── PersistentQueueItem.sq │ └── PersistentQueueMetadata.sq │ ├── songfeed │ ├── SongFeedFilter.sq │ ├── SongFeedRow.sq │ └── SongFeedRowItem.sq │ └── widget │ └── AndroidWidget.sq ├── desktopMain └── kotlin │ ├── SpMp.desktop.kt │ └── com │ └── toasterofbread │ └── spmp │ ├── model │ └── appaction │ │ └── shortcut │ │ └── DefaultShortcuts.desktop.kt │ ├── platform │ ├── AppContext.desktop.kt │ ├── DiscordStatus.desktop.kt │ ├── ImageBitmap.desktop.kt │ ├── SqlDriver.desktop.kt │ ├── VideoPlayback.desktop.kt │ ├── WebViewLogin.desktop.kt │ ├── download │ │ ├── LocalSongMetadataProcessor.desktop.kt │ │ └── PlayerDownloadManager.desktop.kt │ ├── ffmpeg │ │ ├── VideoPlayerFFmpeg.kt │ │ └── kffmpeg.kt │ ├── playerservice │ │ ├── LocalServer.desktop.kt │ │ └── SpMs.desktop.kt │ └── visualiser │ │ └── MusicVisualiser.desktop.kt │ └── ui │ └── layout │ └── nowplaying │ └── overlay │ └── NotifImageOverlayMenu.desktop.kt ├── jvmMain └── kotlin │ ├── Coroutines.jvm.kt │ └── com │ └── toasterofbread │ └── spmp │ ├── model │ ├── mediaitem │ │ └── library │ │ │ └── LocalSongSyncLoader.jvm.kt │ └── settings │ │ └── category │ │ └── StreamingSettings.jvm.kt │ ├── platform │ ├── WebViewLogin.jvm.kt │ └── download │ │ ├── JAudioTaggerMetadataProcessor.kt │ │ ├── LocalSongMetadataProcessor.kt │ │ └── SongDownloader.kt │ ├── ui │ └── layout │ │ └── youtubemusiclogin │ │ └── YoutubeMusicWebviewLogin.jvm.kt │ └── youtubeapi │ └── lyrics │ └── Furigana.jvm.kt ├── main └── res │ ├── values-ldrtl │ └── bools.xml │ └── values │ └── bools.xml ├── notAndroidMain └── kotlin │ └── com │ └── toasterofbread │ └── spmp │ └── platform │ ├── PlatformService.notAndroid.kt │ ├── PlayerListener.notAndroid.kt │ └── playerservice │ ├── DesktopMediaSession.kt │ ├── PlatformExternalPlayerService.notAndroid.kt │ └── PlatformInternalPlayerService.notAndroid.kt └── wasmJsMain └── kotlin ├── Coroutines.wasmJs.kt ├── PlatformTheme.wasmJs.kt ├── SpMp.wasmJs.kt └── com └── toasterofbread └── spmp ├── model ├── appaction │ └── shortcut │ │ └── DefaultShortcuts.wasmJs.kt ├── mediaitem │ └── library │ │ └── LocalSongSyncLoader.wasmJs.kt └── settings │ └── category │ └── StreamingSettings.wasmJs.kt ├── platform ├── AppContext.wasmJs.kt ├── DiscordStatus.wasmJs.kt ├── ImageBitmap.wasmJs.kt ├── SqlDriver.wasmJs.kt ├── VideoPlayback.wasmJs.kt ├── WebViewLogin.wasmJs.kt ├── download │ └── PlayerDownloadManager.wasmJs.kt ├── playerservice │ ├── LocalServer.wasmJs.kt │ └── SpMs.wasmJs.kt └── visualiser │ └── MusicVisualiser.wasmJs.kt ├── ui └── layout │ ├── nowplaying │ └── overlay │ │ └── NotifImageOverlayMenu.wasmJs.kt │ └── youtubemusiclogin │ └── YoutubeMusicWebviewLogin.wasmJs.kt └── youtubeapi └── lyrics └── Furigana.wasmJs.kt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: toasterofbread 2 | ko_fi: toasterofbread 3 | liberapay: toasterofbread 4 | -------------------------------------------------------------------------------- /.github/workflows/build-linux.yml: -------------------------------------------------------------------------------- 1 | name: Build [Linux] 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | types: [opened, synchronize, reopened, ready_for_review] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.event.pull_request.draft == false && (github.event_name == 'workflow_dispatch' || !contains(github.event.head_commit.message, 'noci')) }} 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | submodules: recursive 20 | 21 | - name: Set up JDKs 22 | uses: actions/setup-java@v3 23 | with: 24 | distribution: 'temurin' 25 | java-version: 23 26 | 27 | - name: Build tarball 28 | run: ./gradlew desktopApp:packageReleaseTarball 29 | 30 | - name: Upload tarball artifact 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: spmp-linux-release 34 | path: desktopApp/build/outputs/*.tar.gz 35 | 36 | - name: Rename output file 37 | run: mv desktopApp/build/outputs/*.tar.gz desktopApp/build/outputs/spmp-nightly-linux-x86_64.tar.gz 38 | 39 | - name: Get current date and time 40 | id: date 41 | run: echo "::set-output name=date::$(date +'%Y-%m-%d %H:%M:%S')" 42 | 43 | - name: Update nightly release 44 | uses: mini-bomba/create-github-release@v1.1.3 45 | if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' 46 | with: 47 | token: ${{ secrets.GITHUB_TOKEN }} 48 | tag: nightly-latest 49 | name: Nightly ${{ steps.date.outputs.date }} 50 | files: desktopApp/build/outputs/spmp-nightly-linux-x86_64.tar.gz 51 | clear_attachments: false 52 | -------------------------------------------------------------------------------- /.github/workflows/check_waiting_issue.yml: -------------------------------------------------------------------------------- 1 | name: Check if 'Waiting for response' issue is stale 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | issue_comment: 7 | types: 8 | - created 9 | workflow_dispatch: 10 | 11 | jobs: 12 | issue-manager: 13 | runs-on: ubuntu-latest 14 | permissions: write-all 15 | steps: 16 | - uses: tiangolo/issue-manager@0.4.0 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | config: '{"Waiting for response": {"delay": "P30DT0H0M0S", "message": "Closing because it has been 30 days with no additional information."}}' 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | /.fleet/ 6 | .DS_Store 7 | **/build 8 | /captures 9 | .externalNativeBuild 10 | .cxx 11 | local.properties 12 | .vscode 13 | *.log 14 | /.kotlin 15 | /kotlin-js-store 16 | 17 | app/debug/ 18 | app/release/ 19 | 20 | desktopApp/MediaDeviceSalts* 21 | 22 | buildSrc/project_debug.properties 23 | debug_keys.properties 24 | androidApp/keystore.properties 25 | androidApp/main.keystore 26 | shared/src/commonMain/kotlin/test.kt 27 | ytm-kt 28 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/.gitmodules -------------------------------------------------------------------------------- /.idea/icon.svg: -------------------------------------------------------------------------------- 1 | ../metadata/en-US/images/icon.svg -------------------------------------------------------------------------------- /androidApp/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /androidApp/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/debug.keystore -------------------------------------------------------------------------------- /androidApp/keystore.properties.debug: -------------------------------------------------------------------------------- 1 | storePassword=androiddebug 2 | keyPassword=androiddebug 3 | keyAlias=androiddebug 4 | storeFile=debug.keystore -------------------------------------------------------------------------------- /androidApp/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /androidApp/src/main/java/com/toasterofbread/spmp/widget/LyricsLineHorizontalWidgetReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget 2 | 3 | class LyricsLineHorizontalWidgetReceiver: SpMpWidgetReceiver(SpMpWidgetType.LYRICS_LINE_HORIZONTAL) 4 | -------------------------------------------------------------------------------- /androidApp/src/main/java/com/toasterofbread/spmp/widget/SongQueueWidgetReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget 2 | 3 | class SongQueueWidgetReceiver: SpMpWidgetReceiver(SpMpWidgetType.SONG_QUEUE) 4 | -------------------------------------------------------------------------------- /androidApp/src/main/java/com/toasterofbread/spmp/widget/SpMpWidgetReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget 2 | 3 | import androidx.glance.appwidget.GlanceAppWidget 4 | import androidx.glance.appwidget.GlanceAppWidgetReceiver 5 | 6 | abstract class SpMpWidgetReceiver(type: SpMpWidgetType): GlanceAppWidgetReceiver() { 7 | override val glanceAppWidget: GlanceAppWidget = type.widgetClass.java.getDeclaredConstructor().newInstance() 8 | } 9 | -------------------------------------------------------------------------------- /androidApp/src/main/java/com/toasterofbread/spmp/widget/SplitImageControlsWidgetReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget 2 | 3 | class SplitImageControlsWidgetReceiver: SpMpWidgetReceiver(SpMpWidgetType.SPLIT_IMAGE_CONTROLS) 4 | -------------------------------------------------------------------------------- /androidApp/src/main/java/com/toasterofbread/spmp/widget/WidgetTypeMapper.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget 2 | 3 | import android.content.ComponentName 4 | import androidx.glance.appwidget.GlanceAppWidgetReceiver 5 | import kotlin.reflect.KClass 6 | 7 | internal fun SpMpWidgetType.getWidgetReceiverClass(): KClass = 8 | when (this) { 9 | SpMpWidgetType.LYRICS_LINE_HORIZONTAL -> LyricsLineHorizontalWidgetReceiver::class 10 | SpMpWidgetType.SONG_QUEUE -> SongQueueWidgetReceiver::class 11 | SpMpWidgetType.SPLIT_IMAGE_CONTROLS -> SplitImageControlsWidgetReceiver::class 12 | } 13 | 14 | internal fun getSpMpWidgetTypeForActivityInfo(provider: ComponentName): SpMpWidgetType = 15 | SpMpWidgetType.entries.firstOrNull { it.getWidgetReceiverClass().qualifiedName == provider.className } 16 | ?: throw RuntimeException("No SpMpWidgetType found for $provider") 17 | -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable-ja/widget_preview_song_queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/drawable-ja/widget_preview_song_queue.png -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable/ic_spmp.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 11 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable/widget_preview_image_and_controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/drawable/widget_preview_image_and_controls.png -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable/widget_preview_lyrics_line_horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/drawable/widget_preview_lyrics_line_horizontal.png -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable/widget_preview_song_queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/drawable/widget_preview_song_queue.png -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidApp/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | "横書き歌詞行" 4 | 5 | 曲キュー 6 | 7 | 画像とコントロール 8 | 9 | -------------------------------------------------------------------------------- /androidApp/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /androidApp/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #624C9A 4 | -------------------------------------------------------------------------------- /androidApp/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Horizontal lyrics line 4 | 5 | Song queue 6 | 7 | Image and controls 8 | 9 | 10 | -------------------------------------------------------------------------------- /androidApp/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /androidApp/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /androidApp/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /androidApp/src/main/res/xml/lyrics_line_horizontal_widget_provider.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /androidApp/src/main/res/xml/song_queue_widget_provider.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /androidApp/src/main/res/xml/split_image_controls_widget_provider.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // this is necessary to avoid the plugins to be loaded multiple times 3 | // in each subproject's classloader 4 | kotlin("multiplatform") apply false 5 | kotlin("plugin.compose") apply false 6 | id("com.android.application") apply false 7 | id("com.android.library") apply false 8 | id("org.jetbrains.compose") apply false 9 | } 10 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | gradlePluginPortal() 7 | maven("https://jitpack.io") 8 | } 9 | 10 | dependencies { 11 | implementation("com.github.gmazzo.buildconfig:plugin:5.4.0") 12 | implementation("xmlpull:xmlpull:1.1.3.1") 13 | implementation("com.github.kobjects:kxml2:2.4.1") 14 | } 15 | 16 | tasks.withType(JavaCompile::class) { 17 | options.release.set(23) 18 | } 19 | -------------------------------------------------------------------------------- /buildSrc/project.properties: -------------------------------------------------------------------------------- 1 | PASTE_EE_TOKEN="aumikLoYcGxMX3hdO5AandSr1BEj5aZeiMWSDTdNj" 2 | SUPABASE_URL="https://opdupqbpxdfaqgdffyun.supabase.co" 3 | SUPABASE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9wZHVwcWJweGRmYXFnZGZmeXVuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTYyNzI3NDYsImV4cCI6MjAxMTg0ODc0Nn0.WkTytIhPEoa81iXkpEWl-q9gzJJugYtmZioi6yBlHWk" 4 | DISCORD_APPLICATION_ID="1081929293979992134" 5 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/buildSrc/settings.gradle.kts -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/plugins/shared/Command.kt: -------------------------------------------------------------------------------- 1 | package plugin.shared 2 | 3 | import org.gradle.api.Project 4 | import java.io.ByteArrayOutputStream 5 | 6 | val Project.Command: CommandClass get() = CommandClass(this) 7 | 8 | class CommandClass(project: Project): Project by project { 9 | fun cmd(vararg args: String): String { 10 | val out = ByteArrayOutputStream() 11 | exec { 12 | commandLine(args.toList()) 13 | standardOutput = out 14 | } 15 | return out.toString().trim() 16 | } 17 | 18 | fun getCurrentGitTag(): String? { 19 | try { 20 | val tags: List = cmd("git", "tag", "--points-at", "HEAD").split('\n') 21 | return tags.lastOrNull() 22 | } 23 | catch (e: Throwable) { 24 | return null 25 | } 26 | } 27 | 28 | fun getCurrentGitCommitHash(): String? { 29 | try { 30 | return cmd("git", "rev-parse", "HEAD").ifBlank { null } 31 | } 32 | catch (e: Throwable) { 33 | return null 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/plugins/spmp/DependencyInfo.kt: -------------------------------------------------------------------------------- 1 | package plugin.spmp 2 | 3 | data class DependencyInfo( 4 | val version: String?, 5 | val name: String, 6 | val author: String, 7 | val url: String, 8 | val license: String, 9 | val license_url: String, 10 | val fork_url: String? = null, 11 | val redirect: String? = null 12 | ) 13 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/plugins/spmp/ProjectConfigValues.kt: -------------------------------------------------------------------------------- 1 | package plugin.spmp 2 | 3 | object ProjectConfigValues { 4 | val CONFIG_VALUES = mapOf( 5 | "PASTE_EE_TOKEN" to "String", 6 | "SUPABASE_URL" to "String", 7 | "SUPABASE_KEY" to "String", 8 | "DISCORD_APPLICATION_ID" to "String" 9 | ) 10 | 11 | val DEBUG_CONFIG_VALUES = mapOf( 12 | "YTM_CHANNEL_ID" to "String", 13 | "YTM_COOKIE" to "String", 14 | "YTM_HEADERS" to "String", 15 | "DISCORD_ACCOUNT_TOKEN" to "String", 16 | "DISCORD_ERROR_REPORT_WEBHOOK" to "String", 17 | "DISCORD_STATUS_TEXT_NAME_OVERRIDE" to "String", 18 | "DISCORD_STATUS_TEXT_TEXT_A_OVERRIDE" to "String", 19 | "DISCORD_STATUS_TEXT_TEXT_B_OVERRIDE" to "String", 20 | "DISCORD_STATUS_TEXT_TEXT_C_OVERRIDE" to "String", 21 | "DISCORD_STATUS_TEXT_BUTTON_SONG_OVERRIDE" to "String", 22 | "DISCORD_STATUS_TEXT_BUTTON_PROJECT_OVERRIDE" to "String", 23 | "MUTE_PLAYER" to "Boolean", 24 | "DISABLE_PERSISTENT_QUEUE" to "Boolean", 25 | "STATUS_WEBHOOK_URL" to "String", 26 | "STATUS_WEBHOOK_PAYLOAD" to "String", 27 | "SERVER_PORT" to "Int" 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /desktopApp/appimage/AppRun: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Check if Java is installed 4 | if ! command -v java > /dev/null; then 5 | echo "Java does not appear to be installed on this system. See https://spmp.toastbits.dev/docs/latest/client/installation/ for more information." 6 | exit 1 7 | fi 8 | 9 | SELF=$(readlink -f "$0") 10 | HERE=${SELF%/*} 11 | EXEC=$(grep -e '^Exec=.*' "${HERE}"/*.desktop | head -n 1 | cut -d "=" -f 2 | cut -d " " -f 1) 12 | 13 | cd $HERE 14 | 15 | echo "Running SpMp..." 16 | exec "${EXEC}" "$@" 17 | -------------------------------------------------------------------------------- /desktopApp/appimage/spmp.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Version=1.0 4 | Name=SpMp 5 | Exec=bin/spmp 6 | Icon=spmp 7 | Terminal=false 8 | Categories=Audio;AudioVideo; 9 | -------------------------------------------------------------------------------- /desktopApp/src/jvmMain/kotlin/ErrorDialog.kt: -------------------------------------------------------------------------------- 1 | import java.awt.* 2 | import java.awt.datatransfer.StringSelection 3 | import java.awt.event.ActionEvent 4 | import java.awt.event.ActionListener 5 | 6 | class ExceptionDialog(owner: Frame, exception: Throwable): Dialog(owner, "Error", true) { 7 | init { 8 | layout = BorderLayout() 9 | 10 | val text_area: TextArea = TextArea() 11 | text_area.text = exception.toString() 12 | text_area.append("\n\n") 13 | text_area.append("Stack trace:\n") 14 | exception.stackTraceToString().lines().forEach { text_area.append("$it\n") } 15 | add(text_area, BorderLayout.CENTER) 16 | 17 | val close_button: Button = Button("Close") 18 | close_button.addActionListener { e: ActionEvent -> dispose() } 19 | 20 | val copy_button: Button = Button("Copy error") 21 | copy_button.addActionListener { 22 | val selection: StringSelection = StringSelection(text_area.text) 23 | Toolkit.getDefaultToolkit().systemClipboard.setContents(selection, selection) 24 | } 25 | 26 | val button_panel: Panel = Panel() 27 | button_panel.add(copy_button) 28 | button_panel.add(close_button) 29 | add(button_panel, BorderLayout.SOUTH) 30 | 31 | pack() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docker-image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | ARG GRADLE_VERSION 6 | RUN test -n "$GRADLE_VERSION" 7 | 8 | ARG ANDROID_SDK_VERSION 9 | RUN test -n "$ANDROID_SDK_VERSION" 10 | 11 | RUN apt-get update && \ 12 | apt-get install -y openjdk-21-jre wget git unzip binutils desktop-file-utils 13 | 14 | RUN wget https://download.oracle.com/java/23/latest/jdk-23_linux-x64_bin.deb && \ 15 | apt-get install -y ./jdk-23_linux-x64_bin.deb 16 | 17 | RUN wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O /appimagetool && \ 18 | chmod +x /appimagetool && \ 19 | echo "#!/bin/sh" >> /usr/local/bin/appimagetool && \ 20 | echo '/appimagetool --appimage-extract-and-run "$@"' >> /usr/local/bin/appimagetool && \ 21 | chmod +x /usr/local/bin/appimagetool 22 | 23 | ENV ANDROID_HOME=/android-sdk 24 | RUN wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O cmdlinetools.zip && \ 25 | unzip cmdlinetools.zip && \ 26 | mkdir -p $ANDROID_HOME/cmdline-tools && \ 27 | mv cmdline-tools $ANDROID_HOME/cmdline-tools/latest 28 | 29 | RUN yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses && \ 30 | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "build-tools;$ANDROID_SDK_VERSION.0.0" "platforms;android-$ANDROID_SDK_VERSION" 31 | 32 | RUN wget https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip -O gradle-bin.zip && \ 33 | unzip gradle-bin.zip 34 | 35 | ENV GRADLE_HOME=/gradle-$GRADLE_VERSION 36 | ENV GRADLE_USER_HOME=/gradle-user-home 37 | 38 | ENV JAVA_21_HOME=/usr/lib/jvm/java-21-openjdk-amd64 39 | ENV JAVA_23_HOME=/usr/lib/jvm/jdk-23.0.2-oracle-x64 40 | ENV JAVA_HOME=$JAVA_23_HOME 41 | 42 | WORKDIR /src 43 | ENTRYPOINT ["/src/docker-image/gradleEntryPoint.sh"] 44 | -------------------------------------------------------------------------------- /docker-image/buildAllAndroid.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | docker-image/dockerBuild.sh androidApp:packageRelease -PenableApkSplit 5 | -------------------------------------------------------------------------------- /docker-image/buildAllDesktop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | docker-image/dockerBuild.sh desktopApp:packageReleaseUberJarForCurrentOS desktopApp:packageReleaseAppImage desktopApp:packageReleaseTarball 5 | -------------------------------------------------------------------------------- /docker-image/dockerBuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | IMAGE="spmp-build" 5 | DOCKER_ARGS="$@" 6 | PROJECT=$(pwd) 7 | 8 | GRADLE_VERSION=$(grep -oP '(?<=gradle-)\d+(\.\d+)+(?=-bin\.zip)' $PROJECT/gradle/wrapper/gradle-wrapper.properties) 9 | ANDROID_SDK_VERSION=$(grep "^android.compileSdk=" $PROJECT/gradle.properties | cut -d'=' -f2) 10 | 11 | docker build -t $IMAGE --build-arg="GRADLE_VERSION=$GRADLE_VERSION" --build-arg="ANDROID_SDK_VERSION=$ANDROID_SDK_VERSION" docker-image 12 | 13 | ./gradlew clean 14 | ./gradlew --stop 15 | docker run --rm -it -v $PROJECT:/src -v ~/.gradle:/gradle-user-home $IMAGE:latest $DOCKER_ARGS -PGIT_TAG_OVERRIDE=$TAG 16 | -------------------------------------------------------------------------------- /docker-image/gradleEntryPoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | $GRADLE_HOME/bin/gradle --no-daemon "$@" 3 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Gradle 2 | org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" 3 | org.gradle.caching=false 4 | org.gradle.configureondemand=true 5 | 6 | # Kotlin 7 | kotlin.code.style=official 8 | kotlin.mpp.enableCInteropCommonization=true 9 | kotlin.mpp.enableCInteropCommonization.nowarn=true 10 | kotlin.mpp.androidGradlePluginCompatibility.nowarn=true 11 | 12 | # Android 13 | android.useAndroidX=true 14 | android.compileSdk=35 15 | android.targetSdk=34 16 | android.minSdk=26 17 | android.nonTransitiveRClass=true 18 | android.enableR8.fullMode=true 19 | android.suppressUnsupportedCompileSdk=34,35 20 | 21 | # Compose 22 | org.jetbrains.compose.experimental.wasm.enabled=true 23 | 24 | # Plugin versions 25 | kotlin.version=2.1.10 26 | agp.version=8.8.2 27 | compose.version=1.8.0-alpha01 28 | sqldelight.version=2.0.2 29 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jul 19 17:43:19 GMT 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /metadata/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Features: 2 | 3 | - Edit song, artist, and playlist titles 4 | - Set separate languages for app UI and metadata like song titles 5 | - In-app YouTube Music login 6 | - Display time-synchronised lyrics from [KuGou](https://www.kugou.com/) 7 | - Timed lyrics are displayed in a toggleable bar above every app page 8 | - Furigana (readings) display above Japanese kanji within lyrics 9 | - Select multiple songs for batch actions on any screen 10 | - Pin any song, playlist, album, or artist to the top of the main page 11 | - Customisable Discord rich presence 12 | - Easily insert songs at any position in the queue 13 | -------------------------------------------------------------------------------- /metadata/en-US/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/icon.ico -------------------------------------------------------------------------------- /metadata/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/icon.png -------------------------------------------------------------------------------- /metadata/en-US/images/icon_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/icon_round.png -------------------------------------------------------------------------------- /metadata/en-US/images/icon_simple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/phoneScreenshots/0.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/phoneScreenshots/5.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/phoneScreenshots/6.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/phoneScreenshots/7.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/phoneScreenshots/8.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/phoneScreenshots/9.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/landscape_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/phoneScreenshots/landscape_0.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/landscape_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/phoneScreenshots/landscape_1.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/landscape_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/metadata/en-US/images/phoneScreenshots/landscape_2.png -------------------------------------------------------------------------------- /metadata/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | A YouTube Music client with a focus on language and metadata customisation -------------------------------------------------------------------------------- /metadata/en-US/title.txt: -------------------------------------------------------------------------------- 1 | SpMp -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/SpMp.android.kt: -------------------------------------------------------------------------------- 1 | actual fun isWindowTransparencySupported(): Boolean = false 2 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/model/appaction/shortcut/DefaultShortcuts.android.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.appaction.shortcut 2 | 3 | actual fun getPlatformDefaultShortcuts(): List = emptyList() 4 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/SqlDriver.android.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import androidx.sqlite.db.SupportSQLiteDatabase 4 | import app.cash.sqldelight.db.SqlDriver 5 | import app.cash.sqldelight.driver.android.AndroidSqliteDriver 6 | import com.toasterofbread.spmp.db.Database 7 | 8 | actual fun AppContext.getSqlDriver(): SqlDriver = 9 | AndroidSqliteDriver( 10 | Database.Schema, 11 | ctx, 12 | "spmp_database.db", 13 | callback = object : AndroidSqliteDriver.Callback(Database.Schema) { 14 | override fun onOpen(db: SupportSQLiteDatabase) { 15 | db.setForeignKeyConstraintsEnabled(true) 16 | } 17 | } 18 | ) 19 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/download/LocalSongMetadataProcessor.android.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.download 2 | 3 | actual val LocalSongMetadataProcessor: MetadataProcessor = JAudioTaggerMetadataProcessor 4 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/LocalServer.android.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice 2 | 3 | import com.toasterofbread.spmp.platform.AppContext 4 | import kotlinx.coroutines.Job 5 | 6 | actual object LocalServer { 7 | actual fun isAvailable(): Boolean = false 8 | 9 | actual suspend fun getLocalServerUnavailabilityReason(): String? = null 10 | 11 | actual suspend fun startLocalServer( 12 | context: AppContext, 13 | port: Int, 14 | ): Result = throw IllegalAccessError() 15 | } 16 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformInternalPlayerService.android.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.graphics.Color 6 | import com.toasterofbread.spmp.platform.AppContext 7 | import ProgramArguments 8 | import androidx.annotation.OptIn 9 | import androidx.media3.common.ForwardingPlayer 10 | import androidx.media3.common.Player 11 | import androidx.media3.common.util.UnstableApi 12 | 13 | actual class PlatformInternalPlayerService: ForegroundPlayerService(play_when_ready = true), PlayerService { 14 | actual companion object: InternalPlayerServiceCompanion(PlatformInternalPlayerService::class), PlayerServiceCompanion { 15 | override fun isAvailable(context: AppContext, launch_arguments: ProgramArguments): Boolean = true 16 | override fun playsAudio(): Boolean = true 17 | } 18 | 19 | @Composable 20 | actual override fun Visualiser( 21 | colour: Color, 22 | modifier: Modifier, 23 | opacity: Float, 24 | ) { 25 | super.Visualiser(colour, modifier, opacity) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlayerServiceNotificationCustomAction.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice 2 | 3 | import com.toasterofbread.spmp.model.mediaitem.song.Song 4 | import com.toasterofbread.spmp.model.mediaitem.song.updateLiked 5 | import com.toasterofbread.spmp.platform.AppContext 6 | import dev.toastbits.ytmkt.endpoint.SetSongLikedEndpoint 7 | import dev.toastbits.ytmkt.model.external.SongLikedStatus 8 | 9 | enum class PlayerServiceNotificationCustomAction { 10 | LIKE, 11 | UNLIKE; 12 | 13 | suspend fun execute(player: PlayerService, context: AppContext) { 14 | when (this) { 15 | LIKE, 16 | UNLIKE -> { 17 | val song: Song = player.getSong() ?: return 18 | 19 | val target_liked: SongLikedStatus = 20 | when (this) { 21 | LIKE -> SongLikedStatus.LIKED 22 | UNLIKE -> SongLikedStatus.NEUTRAL 23 | } 24 | 25 | val set_liked_endpoint: SetSongLikedEndpoint? = context.ytapi.user_auth_state?.SetSongLiked 26 | song.updateLiked(target_liked, set_liked_endpoint, context).getOrThrow() 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/SpMs.android.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice 2 | 3 | import com.toasterofbread.spmp.platform.AppContext 4 | 5 | actual fun getSpMsMachineId(context: AppContext): String { 6 | return getSpMsMachineIdFromFile(context.getFilesDir()!!.resolve("spmp_machine_id.txt")) 7 | } 8 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/createDataSourceFactory.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice 2 | 3 | import androidx.annotation.OptIn 4 | import androidx.media3.common.util.UnstableApi 5 | import androidx.media3.datasource.DataSource 6 | import androidx.media3.datasource.DataSpec 7 | import androidx.media3.datasource.DefaultDataSource 8 | import androidx.media3.datasource.ResolvingDataSource 9 | import kotlinx.coroutines.runBlocking 10 | import java.io.IOException 11 | 12 | @OptIn(UnstableApi::class) 13 | internal fun ForegroundPlayerService.createDataSourceFactory(processor: MediaDataSpecProcessor): DataSource.Factory { 14 | return ResolvingDataSource.Factory({ 15 | DefaultDataSource.Factory(this).createDataSource() 16 | }) { data_spec: DataSpec -> 17 | try { 18 | return@Factory runBlocking { 19 | processor.processMediaDataSpec(data_spec).also { 20 | loudness_enhancer?.update(current_song, context) 21 | } 22 | } 23 | } 24 | catch (e: Throwable) { 25 | throw IOException(e) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/notification/NotificationState.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice.notification 2 | 3 | import android.media.session.PlaybackState 4 | import dev.toastbits.ytmkt.model.external.SongLikedStatus 5 | 6 | data class NotificationState( 7 | val playback_state: Int? = PlaybackState.STATE_NONE, 8 | val paused: Boolean = true, 9 | val current_liked_status: SongLikedStatus? = null, 10 | val authenticated: Boolean = false, 11 | val position_ms: Long? = null 12 | ) 13 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/visualiser/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dániel Zolnai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/ui/SongLikedStatusIcon.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui 2 | 3 | import androidx.annotation.DrawableRes 4 | import com.toasterofbread.spmp.shared.R 5 | import dev.toastbits.ytmkt.model.external.SongLikedStatus 6 | 7 | @DrawableRes 8 | fun SongLikedStatus?.getAndroidIcon(authenticated: Boolean): Int = 9 | if (authenticated) 10 | when (this) { 11 | null, 12 | SongLikedStatus.NEUTRAL -> R.drawable.ic_thumb_up_off 13 | SongLikedStatus.LIKED -> R.drawable.ic_thumb_up 14 | SongLikedStatus.DISLIKED -> R.drawable.ic_thumb_down 15 | } 16 | else 17 | when (this) { 18 | null, 19 | SongLikedStatus.DISLIKED, 20 | SongLikedStatus.NEUTRAL -> R.drawable.ic_heart_off 21 | SongLikedStatus.LIKED -> R.drawable.ic_heart 22 | } 23 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/widget/WidgetActionCallback.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget 2 | 3 | import android.content.Context 4 | import androidx.glance.GlanceId 5 | import androidx.glance.action.Action 6 | import androidx.glance.action.ActionParameters 7 | import androidx.glance.action.actionParametersOf 8 | import androidx.glance.appwidget.action.ActionCallback 9 | import androidx.glance.appwidget.action.actionRunCallback 10 | import com.toasterofbread.spmp.widget.action.TypeWidgetClickAction 11 | import com.toasterofbread.spmp.widget.action.WidgetClickAction 12 | import com.toasterofbread.spmp.widget.configuration.SpMpWidgetConfiguration 13 | import kotlinx.serialization.encodeToString 14 | 15 | internal class WidgetActionCallback: ActionCallback { 16 | override suspend fun onAction( 17 | context: Context, 18 | glanceId: GlanceId, 19 | parameters: ActionParameters 20 | ) { 21 | val serialisedAction: String = parameters[keyAction] ?: return 22 | val action: WidgetClickAction = SpMpWidgetConfiguration.json.decodeFromString(serialisedAction) 23 | SpMpWidget.runActionOnWidget(action, glanceId) 24 | } 25 | 26 | companion object { 27 | val keyAction: ActionParameters.Key = ActionParameters.Key("action") 28 | 29 | operator fun invoke(action: WidgetClickAction<*>): Action = 30 | actionRunCallback( 31 | actionParametersOf( 32 | keyAction to SpMpWidgetConfiguration.json.encodeToString(action) 33 | ) 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/widget/action/QueueSeekAction.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.action 2 | 3 | import SpMp 4 | import android.content.Context 5 | import androidx.glance.GlanceId 6 | import androidx.glance.action.Action 7 | import androidx.glance.action.ActionParameters 8 | import androidx.glance.action.actionParametersOf 9 | import androidx.glance.appwidget.action.ActionCallback 10 | import androidx.glance.appwidget.action.actionRunCallback 11 | import com.toasterofbread.spmp.platform.playerservice.PlayerService 12 | import kotlinx.coroutines.DelicateCoroutinesApi 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.GlobalScope 15 | import kotlinx.coroutines.launch 16 | 17 | class QueueSeekAction: ActionCallback { 18 | @OptIn(DelicateCoroutinesApi::class) 19 | override suspend fun onAction( 20 | context: Context, 21 | glanceId: GlanceId, 22 | parameters: ActionParameters 23 | ) { 24 | val index: Int = parameters[keyIndex] ?: return 25 | val controller: PlayerService = SpMp._player_state?.controller ?: return 26 | 27 | GlobalScope.launch(Dispatchers.Main) { 28 | controller.seekToItem(index) 29 | } 30 | } 31 | 32 | companion object { 33 | val keyIndex: ActionParameters.Key = ActionParameters.Key("index") 34 | 35 | operator fun invoke(index: Int): Action = 36 | actionRunCallback( 37 | actionParametersOf(keyIndex to index) 38 | ) 39 | } 40 | } -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/widget/component/GlanceCanvas.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.component 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.geometry.Size 5 | import androidx.compose.ui.graphics.Canvas 6 | import androidx.compose.ui.graphics.ImageBitmap 7 | import androidx.compose.ui.graphics.asAndroidBitmap 8 | import androidx.compose.ui.platform.LocalDensity 9 | import androidx.compose.ui.unit.DpSize 10 | import androidx.compose.ui.unit.dp 11 | import androidx.glance.GlanceModifier 12 | import androidx.glance.Image 13 | import androidx.glance.ImageProvider 14 | 15 | @Composable 16 | fun GlanceCanvas(size: DpSize, modifier: GlanceModifier, draw: @Composable Canvas.(Size) -> Unit) { 17 | if (size.width <= 0.dp || size.height <= 0.dp) { 18 | return 19 | } 20 | 21 | val image: ImageBitmap = 22 | with (LocalDensity.current) { 23 | ImageBitmap(size.width.roundToPx() + 20, size.height.roundToPx()) 24 | } 25 | 26 | val canvas: Canvas = Canvas(image) 27 | draw(canvas, Size(image.width.toFloat(), image.height.toFloat())) 28 | 29 | Image( 30 | ImageProvider(image.asAndroidBitmap()), 31 | null, 32 | modifier 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/widget/component/GlanceLargePlayPauseButton.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.component 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.unit.dp 5 | import androidx.glance.GlanceModifier 6 | import androidx.glance.Image 7 | import androidx.glance.ImageProvider 8 | import androidx.glance.action.clickable 9 | import androidx.glance.appwidget.cornerRadius 10 | import androidx.glance.background 11 | import androidx.glance.layout.Alignment 12 | import androidx.glance.layout.Box 13 | import com.toasterofbread.spmp.shared.R 14 | import com.toasterofbread.spmp.widget.action.PlayPauseAction 15 | import dev.toastbits.composekit.theme.core.ui.LocalComposeKitTheme 16 | import dev.toastbits.composekit.theme.core.ThemeValues 17 | import dev.toastbits.composekit.theme.core.vibrantAccent 18 | 19 | @Composable 20 | internal fun GlanceLargePlayPauseButton( 21 | play: Boolean, 22 | modifier: GlanceModifier = GlanceModifier 23 | ) { 24 | val theme: ThemeValues = LocalComposeKitTheme.current 25 | Box( 26 | modifier 27 | .background(theme.vibrantAccent) 28 | .cornerRadius(10.dp) 29 | .clickable( 30 | PlayPauseAction(play) 31 | ), 32 | contentAlignment = Alignment.Center 33 | ) { 34 | val icon: Int = 35 | if (play) R.drawable.ic_play 36 | else R.drawable.ic_pause 37 | 38 | Image(ImageProvider(icon), null) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/widget/component/GlanceLazyColumn.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.component 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.runtime.Composable 5 | import androidx.glance.GlanceModifier 6 | import androidx.glance.appwidget.lazy.LazyListScope 7 | import androidx.glance.layout.Spacer 8 | import androidx.glance.layout.height 9 | import com.toasterofbread.spmp.widget.modifier.padding 10 | import dev.toastbits.composekit.components.utils.modifier.horizontal 11 | 12 | @Composable 13 | internal fun GlanceLazyColumn(content_padding: PaddingValues, modifier: GlanceModifier, content: LazyListScope.() -> Unit) { 14 | androidx.glance.appwidget.lazy.LazyColumn( 15 | modifier.padding(content_padding.horizontal) 16 | ) { 17 | item { 18 | Spacer(GlanceModifier.height(content_padding.calculateTopPadding())) 19 | } 20 | content() 21 | item { 22 | Spacer(GlanceModifier.height(content_padding.calculateBottomPadding())) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/widget/impl/LyricsLineHorizontalWidget.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.impl 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.glance.GlanceModifier 5 | import com.toasterofbread.spmp.ui.util.LyricsLineState 6 | 7 | internal class LyricsLineHorizontalWidget: LyricsWidget() { 8 | @Composable 9 | override fun LyricsContent(lyrics: LyricsLineState, modifier: GlanceModifier) { 10 | lyrics.LyricsDisplay { show, line -> 11 | if (show && line != null) { 12 | LyricsLine(line) 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/widget/mapper/FontResourceMapper.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.mapper 2 | 3 | import android.graphics.Typeface 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.platform.LocalContext 6 | import androidx.compose.ui.text.font.Font 7 | 8 | @Composable 9 | @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") 10 | fun Font.toAndroidTypeface(): Typeface = 11 | when (this) { 12 | is androidx.compose.ui.text.font.AndroidAssetFont -> 13 | this.typeface ?: this.loadCached(LocalContext.current)!! 14 | else -> throw NotImplementedError(this::class.toString()) 15 | } 16 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/widget/modifier/padding.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.modifier 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.calculateEndPadding 5 | import androidx.compose.foundation.layout.calculateStartPadding 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.platform.LocalLayoutDirection 8 | import androidx.glance.GlanceModifier 9 | import androidx.glance.layout.padding 10 | 11 | @Composable 12 | fun GlanceModifier.padding(padding_values: PaddingValues): GlanceModifier = 13 | padding( 14 | top = padding_values.calculateTopPadding(), 15 | bottom = padding_values.calculateBottomPadding(), 16 | start = padding_values.calculateStartPadding(LocalLayoutDirection.current), 17 | end = padding_values.calculateEndPadding(LocalLayoutDirection.current) 18 | ) 19 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/widget/modifier/size.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.modifier 2 | 3 | import androidx.compose.ui.unit.DpSize 4 | import androidx.glance.GlanceModifier 5 | import androidx.glance.layout.size 6 | 7 | fun GlanceModifier.size(size: DpSize): GlanceModifier = 8 | size(size.width, size.height) 9 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/toasterofbread/spmp/widget/modifier/systemCornerRadius.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.modifier 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.glance.GlanceModifier 5 | import androidx.glance.appwidget.cornerRadius 6 | 7 | @Composable 8 | fun GlanceModifier.systemCornerRadius(): GlanceModifier { 9 | if (android.os.Build.VERSION.SDK_INT >= 31) { 10 | val systemCornerRadiusDefined: Boolean = 11 | androidx.glance.LocalContext.current.resources.getResourceName(android.R.dimen.system_app_widget_background_radius) != null 12 | if (systemCornerRadiusDefined) { 13 | return cornerRadius(android.R.dimen.system_app_widget_background_radius) 14 | } 15 | } 16 | return this 17 | } 18 | -------------------------------------------------------------------------------- /shared/src/androidMain/res/drawable/ic_heart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shared/src/androidMain/res/drawable/ic_heart_off.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shared/src/androidMain/res/drawable/ic_pause.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shared/src/androidMain/res/drawable/ic_play.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shared/src/androidMain/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shared/src/androidMain/res/drawable/ic_skip_next.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shared/src/androidMain/res/drawable/ic_skip_previous.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shared/src/androidMain/res/drawable/ic_spmp.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 11 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /shared/src/androidMain/res/drawable/ic_thumb_down.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shared/src/androidMain/res/drawable/ic_thumb_up.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shared/src/androidMain/res/drawable/ic_thumb_up_off.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shared/src/androidMain/res/drawable/ic_visibility.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/drawable/ic_discord.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/drawable/ic_github.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/drawable/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/shared/src/commonMain/composeResources/drawable/ic_splash.png -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/drawable/ic_spmp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toasterofbread/spmp/ff7451b6070f8fc927d3decaf466b8b098433123/shared/src/commonMain/composeResources/drawable/ic_spmp.png -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/values-en-rGB/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | English (GB) 3 | 4 | -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/values-en-rUS/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | English (US) 3 | 4 | Energize 5 | Strings that have not been localized for the current language 6 | Customize layout arrangement and content 7 | Customize app colours and change accent source 8 | Manage Discord account and customize status appearance 9 | Synchronized lyrics for the current song 10 | 11 | Pick accent color from image 12 | Accent color 13 | Color background with accent 14 | Color elements with accent 15 | Don't use accent color 16 | Behavior 17 | Lyrics appearance and behavior 18 | Select color 19 | 20 | Romanize furigana (readings) 21 | 22 | Download was canceled 23 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/Coroutines.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.Dispatchers 2 | import kotlinx.coroutines.CoroutineDispatcher 3 | 4 | expect val Dispatchers.PlatformIO: CoroutineDispatcher 5 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/JsonHttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation 5 | import io.ktor.serialization.kotlinx.json.json 6 | import kotlinx.serialization.json.Json 7 | 8 | val JsonHttpClient: HttpClient = 9 | HttpClient() { 10 | // expectSuccess = true 11 | install(ContentNegotiation) { 12 | json( 13 | Json { 14 | ignoreUnknownKeys = true 15 | explicitNulls = false 16 | } 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/appaction/action/navigation/JumpToLyricsNavigationAction.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.appaction.action.navigation 2 | 3 | import kotlinx.serialization.Serializable 4 | import com.toasterofbread.spmp.service.playercontroller.PlayerState 5 | import com.toasterofbread.spmp.ui.layout.nowplaying.overlay.PlayerOverlayMenu 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.filled.Lyrics 11 | 12 | @Serializable 13 | class JumpToLyricsNavigationAction: NavigationAction { 14 | override fun getType(): NavigationAction.Type = 15 | NavigationAction.Type.JUMP_TO_LYRICS 16 | 17 | override fun getIcon(): ImageVector = 18 | Icons.Default.Lyrics 19 | 20 | override suspend fun execute(player: PlayerState) { 21 | player.openNowPlayingPlayerOverlayMenu(PlayerOverlayMenu.getLyricsMenu()) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/appaction/action/navigation/TogglePlayerNavigationAction.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.appaction.action.navigation 2 | 3 | import kotlinx.serialization.Serializable 4 | import com.toasterofbread.spmp.service.playercontroller.PlayerState 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.filled.KeyboardArrowUp 10 | 11 | @Serializable 12 | class TogglePlayerNavigationAction: NavigationAction { 13 | override fun getType(): NavigationAction.Type = 14 | NavigationAction.Type.TOGGLE_PLAYER 15 | 16 | override fun getIcon(): ImageVector = 17 | Icons.Default.KeyboardArrowUp 18 | 19 | override suspend fun execute(player: PlayerState) { 20 | player.expansion.toggle() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/appaction/action/playback/PlayPausePlaybackActions.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.appaction.action.playback 2 | 3 | import kotlinx.serialization.Serializable 4 | import com.toasterofbread.spmp.service.playercontroller.PlayerState 5 | 6 | @Serializable 7 | class PlayPlaybackAppAction: PlaybackAction { 8 | override fun getType(): PlaybackAction.Type = 9 | PlaybackAction.Type.PLAY 10 | 11 | override suspend fun execute(player: PlayerState) { 12 | player.withPlayer { 13 | play() 14 | } 15 | } 16 | } 17 | 18 | @Serializable 19 | class PausePlaybackAppAction: PlaybackAction { 20 | override fun getType(): PlaybackAction.Type = 21 | PlaybackAction.Type.PAUSE 22 | 23 | override suspend fun execute(player: PlayerState) { 24 | player.withPlayer { 25 | pause() 26 | } 27 | } 28 | } 29 | 30 | @Serializable 31 | class TogglePlayPlaybackAppAction: PlaybackAction { 32 | override fun getType(): PlaybackAction.Type = 33 | PlaybackAction.Type.TOGGLE_PLAY 34 | 35 | override suspend fun execute(player: PlayerState) { 36 | player.withPlayer { 37 | playPause() 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/appaction/action/playback/QueuePlaybackActions.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.appaction.action.playback 2 | 3 | import kotlinx.serialization.Serializable 4 | import com.toasterofbread.spmp.service.playercontroller.PlayerState 5 | 6 | @Serializable 7 | class ShuffleQueuePlaybackAppAction: PlaybackAction { 8 | override fun getType(): PlaybackAction.Type = 9 | PlaybackAction.Type.SHUFFLE_QUEUE 10 | 11 | override suspend fun execute(player: PlayerState) { 12 | player.withPlayer{ 13 | undoableAction { 14 | shuffleQueue(start = current_item_index + 1) 15 | } 16 | } 17 | } 18 | } 19 | 20 | @Serializable 21 | class ClearQueuePlaybackAppAction: PlaybackAction { 22 | override fun getType(): PlaybackAction.Type = 23 | PlaybackAction.Type.CLEAR_QUEUE 24 | 25 | override suspend fun execute(player: PlayerState) { 26 | player.withPlayer { 27 | undoableAction { 28 | clearQueue(keep_current = player.status.m_song_count > 1) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/appaction/shortcut/Shortcut.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.appaction.shortcut 2 | 3 | import androidx.compose.foundation.shape.CircleShape 4 | import kotlinx.serialization.Serializable 5 | import com.toasterofbread.spmp.model.appaction.AppAction 6 | import com.toasterofbread.spmp.ui.component.shortcut.trigger.ShortcutTrigger 7 | 8 | val SHORTCUT_INDICATOR_SHAPE get() = CircleShape 9 | const val SHORTCUT_INDICATOR_GROUP_ANIM_DURATION_MS: Long = 30 10 | const val SHORTCUT_INDICATOR_SHOW_DELAY_MS: Long = 200 11 | 12 | @Serializable 13 | data class Shortcut(val trigger: ShortcutTrigger?, val action: AppAction) 14 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/appaction/shortcut/ShortcutIndicator.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.appaction.shortcut 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | fun ShortcutIndicator() { 7 | TODO() 8 | // val shortcut_index: Int? = remember(button_type) { getButtonShortcutButton(button_type, player) } 9 | // if (shortcut_index == null) { 10 | // return@IconButton 11 | // } 12 | 13 | // val show_shortcut_indicator: Boolean by remember(shortcut_index) { derivedStateOf { 14 | // showing_shortcut_indices > shortcut_index 15 | // } } 16 | 17 | // AnimatedVisibility( 18 | // show_shortcut_indicator, 19 | // Modifier.offset(17.dp, 17.dp).zIndex(1f), 20 | // enter = fadeIn(), 21 | // exit = fadeOut() 22 | // ) { 23 | // val indicator_colour: Color = 24 | // if (button == current_button) player.theme.onAccent 25 | // else player.theme.accent 26 | 27 | // Box( 28 | // Modifier.size(20.dp).background(indicator_colour, SHORTCUT_INDICATOR_SHAPE), 29 | // contentAlignment = Alignment.Center 30 | // ) { 31 | // Text( 32 | // (shortcut_index + 1).toString(), 33 | // Modifier.offset(y = (-5).dp), 34 | // fontSize = 10.sp, 35 | // color = indicator_colour.getContrasted() 36 | // ) 37 | // } 38 | // } 39 | } 40 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/ToInfoString.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem 2 | 3 | fun MediaItem.toInfoString(): String { 4 | if (this !is MediaItemData) { 5 | return toString() 6 | } 7 | 8 | val string: StringBuilder = StringBuilder(toString()) 9 | 10 | val values: Map = getDataValues() 11 | if (values.isNotEmpty()) { 12 | string.append(" {") 13 | for (entry in values) { 14 | string.append("\n ${entry.key}=${entry.value}") 15 | } 16 | string.append("\n}") 17 | } 18 | 19 | return string.toString() 20 | } 21 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/UnsupportedPropertyRememberer.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem 2 | 3 | class UnsupportedPropertyRememberer( 4 | private val can_read: Boolean = false, 5 | private val can_write: Boolean = false, 6 | private val getUnsupportedUsageMessage: ((on_read: Boolean) -> String)? = null 7 | ): PropertyRememberer() { 8 | override fun onRead(key: String) { 9 | if (!can_read) { 10 | val message: String = getUnsupportedUsageMessage?.invoke(true) ?: "" 11 | throw UnsupportedOperationException("Property '$key' cannot be read from. $message") 12 | } 13 | } 14 | 15 | override fun onWrite(key: String) { 16 | if (!can_write) { 17 | val message: String = getUnsupportedUsageMessage?.invoke(false) ?: "" 18 | throw UnsupportedOperationException("Property '$key' cannot be written to. $message") 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/artist/Subscribed.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.artist 2 | 3 | import com.toasterofbread.spmp.platform.AppContext 4 | import dev.toastbits.ytmkt.endpoint.SetSubscribedToArtistEndpoint 5 | 6 | suspend fun Artist.updateSubscribed( 7 | subscribed: Boolean, 8 | endpoint: SetSubscribedToArtistEndpoint, 9 | context: AppContext 10 | ): Result = runCatching { 11 | endpoint.setSubscribedToArtist(id, subscribed, SubscribeChannelId.get(context.database)).getOrThrow() 12 | Subscribed.set(subscribed, context.database) 13 | } 14 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/artist/SubscriberCount.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.artist 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.getValue 5 | import com.toasterofbread.spmp.platform.AppContext 6 | import com.toasterofbread.spmp.platform.getUiLanguage 7 | import com.toasterofbread.spmp.platform.observeUiLanguage 8 | import dev.toastbits.composekit.util.model.Locale 9 | import dev.toastbits.ytmkt.uistrings.amountToString 10 | import org.jetbrains.compose.resources.stringResource 11 | import spmp.shared.generated.resources.Res 12 | import spmp.shared.generated.resources.artist_x_subscribers 13 | 14 | fun Artist.getSubscriberCount(context: AppContext): Int? = 15 | context.database.artistQueries.subscriberCountById(id).executeAsOne().subscriber_count?.toInt() 16 | 17 | @Composable 18 | fun Int.toReadableSubscriberCount(context: AppContext): String { 19 | val ui_language: Locale by context.observeUiLanguage() 20 | return stringResource(Res.string.artist_x_subscribers).replace("\$x", amountToString(this, ui_language.toTag())) 21 | } 22 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/artist/formatArtistTitles.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.artist 2 | 3 | import com.toasterofbread.spmp.platform.AppContext 4 | import dev.toastbits.composekit.util.isJa 5 | 6 | fun formatArtistTitles(titles: List, context: AppContext): String? { 7 | val filtered_titles: List = titles.filterNotNull() 8 | if (filtered_titles.isEmpty()) { 9 | return null 10 | } 11 | 12 | val separator: String 13 | if (filtered_titles.any { title -> title.any { char -> char.isJa() } }) { 14 | separator = "、" 15 | } 16 | else { 17 | separator = ", " 18 | } 19 | 20 | return filtered_titles.joinToString(separator) 21 | } 22 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/db/Delete.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.db 2 | 3 | import com.toasterofbread.spmp.db.Database 4 | import com.toasterofbread.spmp.model.mediaitem.MediaItem 5 | import com.toasterofbread.spmp.model.mediaitem.enums.MediaItemType 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | import PlatformIO 9 | 10 | suspend fun MediaItem.removeFromDatabase(db: Database) = withContext(Dispatchers.PlatformIO) { 11 | db.transaction { 12 | db.pinnedItemQueries.remove(id, getType().ordinal.toLong()) 13 | 14 | when (getType()) { 15 | MediaItemType.SONG -> db.songQueries.removeById(id) 16 | MediaItemType.ARTIST -> db.artistQueries.removeById(id) 17 | MediaItemType.PLAYLIST_REM -> { 18 | db.songQueries.dereferenceAlbumById(id) 19 | db.playlistItemQueries.removeByPlaylistId(id) 20 | db.playlistQueries.removeById(id) 21 | } 22 | MediaItemType.PLAYLIST_LOC -> throw IllegalStateException("Local playlists are not stored in database") 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/db/LikedSongs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.db 2 | 3 | import LocalPlayerState 4 | import androidx.compose.runtime.* 5 | import app.cash.sqldelight.Query 6 | import com.toasterofbread.spmp.model.mediaitem.song.Song 7 | import com.toasterofbread.spmp.model.mediaitem.song.SongRef 8 | import com.toasterofbread.spmp.model.mediaitem.song.toLong 9 | import com.toasterofbread.spmp.service.playercontroller.PlayerState 10 | import dev.toastbits.ytmkt.model.external.SongLikedStatus 11 | 12 | @Composable 13 | fun rememberLocalLikedSongs(liked_status: SongLikedStatus = SongLikedStatus.LIKED): State?> { 14 | val player: PlayerState = LocalPlayerState.current 15 | 16 | val query: Query = remember(liked_status) { 17 | player.database.songQueries.byLiked(liked_status.toLong()) 18 | } 19 | val liked_songs: MutableState?> = remember { mutableStateOf(null) } 20 | 21 | DisposableEffect(query) { 22 | val listener: Query.Listener = Query.Listener { 23 | liked_songs.value = query.executeAsList().map { SongRef(it) } 24 | } 25 | 26 | listener.queryResultsChanged() 27 | 28 | query.addListener(listener) 29 | 30 | onDispose { 31 | query.removeListener(listener) 32 | } 33 | } 34 | 35 | return liked_songs 36 | } 37 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/db/ThemeColour.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.db 2 | 3 | import LocalPlayerState 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.derivedStateOf 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.graphics.Color 9 | import dev.toastbits.composekit.util.getThemeColour 10 | import com.toasterofbread.spmp.model.mediaitem.MediaItem 11 | import com.toasterofbread.spmp.model.mediaitem.loader.MediaItemThumbnailLoader 12 | import dev.toastbits.ytmkt.model.external.ThumbnailProvider as YtmThumbnailProvider 13 | 14 | @Composable 15 | fun MediaItem.rememberThemeColour(): Color? { 16 | val player = LocalPlayerState.current 17 | val thumbnail_state = MediaItemThumbnailLoader.rememberItemState(this) 18 | val item_colour: Color? by ThemeColour.observe(player.database) 19 | 20 | val colour: Color? by remember(thumbnail_state, item_colour) { derivedStateOf { 21 | if (item_colour != null) { 22 | return@derivedStateOf item_colour 23 | } 24 | 25 | for (quality in YtmThumbnailProvider.Quality.entries) { 26 | val image = thumbnail_state.loaded_images[quality] ?: continue 27 | return@derivedStateOf image.getThemeColour() 28 | } 29 | return@derivedStateOf null 30 | } } 31 | 32 | return colour 33 | } 34 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/db/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.db 2 | 3 | import com.toasterofbread.spmp.model.mediaitem.MediaItemData 4 | import com.toasterofbread.spmp.model.mediaitem.loader.MediaItemLoader 5 | import com.toasterofbread.spmp.platform.AppContext 6 | 7 | fun Boolean.toSQLBoolean(): Long? = if (this) 0L else null 8 | fun Long?.fromSQLBoolean(): Boolean = this != null 9 | 10 | fun Boolean?.toNullableSQLBoolean(): Long? = 11 | when (this) { 12 | false -> 0L 13 | true -> 1L 14 | null -> null 15 | } 16 | fun Long?.fromNullableSQLBoolean(): Boolean? = 17 | when (this) { 18 | 0L -> false 19 | 1L -> true 20 | else -> null 21 | } 22 | 23 | suspend fun AppContext.loadMediaItemValue(item: ItemType, getValue: ItemType.() -> T?): Result? { 24 | // If the item is marked as already loaded, give up 25 | val loaded = database.mediaItemQueries.loadedById(item.id).executeAsOneOrNull()?.loaded.fromSQLBoolean() 26 | if (loaded) { 27 | return null 28 | } 29 | 30 | // Load item data 31 | val load_result = MediaItemLoader.loadUnknown(item, this) 32 | val loaded_item = load_result.fold( 33 | { it }, 34 | { return Result.failure(it) } 35 | ) 36 | 37 | val value = getValue(loaded_item) 38 | return value?.let { Result.success(it) } 39 | } 40 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/layout/AppMediaItemLayout.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.layout 2 | 3 | import dev.toastbits.ytmkt.model.external.ItemLayoutType 4 | import dev.toastbits.ytmkt.uistrings.UiString 5 | import dev.toastbits.ytmkt.model.external.YoutubePage 6 | import dev.toastbits.ytmkt.model.external.mediaitem.MediaItemLayout 7 | import com.toasterofbread.spmp.model.mediaitem.MediaItemData 8 | import com.toasterofbread.spmp.model.mediaitem.toMediaItemData 9 | 10 | data class AppMediaItemLayout( 11 | val items: List, 12 | val title: UiString?, 13 | val subtitle: UiString?, 14 | val type: ItemLayoutType? = null, 15 | val view_more: YoutubePage? = null 16 | ) { 17 | constructor(layout: MediaItemLayout): this( 18 | items = layout.items.map { it.toMediaItemData() }, 19 | title = layout.title, 20 | subtitle = layout.subtitle, 21 | type = layout.type, 22 | view_more = layout.view_more 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/library/LocalSongSyncLoader.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.library 2 | 3 | import com.toasterofbread.spmp.platform.download.DownloadStatus 4 | 5 | internal expect class LocalSongSyncLoader(): SyncLoader 6 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/loader/ArtistSubscribedLoader.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.loader 2 | 3 | import com.toasterofbread.spmp.model.mediaitem.artist.Artist 4 | import com.toasterofbread.spmp.platform.AppContext 5 | 6 | internal object ArtistSubscribedLoader: ItemStateLoader() { 7 | suspend fun loadArtistSubscribed( 8 | artist: Artist, 9 | context: AppContext 10 | ): Result? = 11 | performLoad(artist.id) { 12 | val result = context.ytapi.user_auth_state!!.SubscribedToArtist.isSubscribedToArtist(artist.id) 13 | artist.Subscribed.set(result.getOrNull(), context.database) 14 | return@performLoad result 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/loader/SongLikedLoader.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.loader 2 | 3 | import com.toasterofbread.spmp.model.mediaitem.song.toLong 4 | import com.toasterofbread.spmp.model.mediaitem.song.toSongLikedStatus 5 | import com.toasterofbread.spmp.platform.AppContext 6 | import dev.toastbits.ytmkt.endpoint.SongLikedEndpoint 7 | import dev.toastbits.ytmkt.model.external.SongLikedStatus 8 | 9 | internal object SongLikedLoader: ItemStateLoader() { 10 | suspend fun loadSongLiked(song_id: String, context: AppContext, endpoint: SongLikedEndpoint?): Result { 11 | return performLoad(song_id) { 12 | if (endpoint?.isImplemented() == true) { 13 | endpoint.getSongLiked(song_id) 14 | .onSuccess { liked -> 15 | context.database.songQueries.updatelikedById(liked.toLong(), song_id) 16 | } 17 | } 18 | else { 19 | Result.success( 20 | context.database.songQueries.likedById(song_id).executeAsOneOrNull()?.liked.toSongLikedStatus() ?: SongLikedStatus.NEUTRAL 21 | ) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/playlist/LocalPlaylistRef.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.playlist 2 | 3 | import com.toasterofbread.spmp.db.Database 4 | import com.toasterofbread.spmp.model.mediaitem.MediaItemRef 5 | import com.toasterofbread.spmp.model.mediaitem.PropertyRememberer 6 | import com.toasterofbread.spmp.model.mediaitem.UnsupportedPropertyRememberer 7 | import com.toasterofbread.spmp.model.mediaitem.library.MediaItemLibrary 8 | import com.toasterofbread.spmp.platform.AppContext 9 | import dev.toastbits.composekit.context.PlatformFile 10 | 11 | class LocalPlaylistRef(override val id: String): LocalPlaylist, MediaItemRef() { 12 | override fun toString(): String = "LocalPlaylistRef($id)" 13 | override fun getReference(): LocalPlaylistRef = this 14 | 15 | override fun createDbEntry(db: Database) { 16 | throw IllegalStateException(id) 17 | } 18 | 19 | override suspend fun loadData(context: AppContext, populate_data: Boolean, force: Boolean, save: Boolean): Result { 20 | return runCatching { 21 | val file: PlatformFile = 22 | MediaItemLibrary.getLocalPlaylistFile(this, context) 23 | ?: throw RuntimeException("Local file not available") 24 | PlaylistFileConverter.loadFromFile(file, context)!! 25 | } 26 | } 27 | 28 | override val property_rememberer: PropertyRememberer = 29 | UnsupportedPropertyRememberer { is_read -> 30 | if (is_read) "Local playlist must be loaded from file into a LocalPlaylistData." 31 | else "Use a PlaylistEditor instead." 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/playlist/RemotePlaylistRef.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.playlist 2 | 3 | import com.toasterofbread.spmp.db.Database 4 | import com.toasterofbread.spmp.model.mediaitem.MediaItemRef 5 | import com.toasterofbread.spmp.model.mediaitem.PropertyRememberer 6 | 7 | class RemotePlaylistRef(override val id: String): RemotePlaylist, MediaItemRef() { 8 | override fun toString(): String = "RemotePlaylistRef($id)" 9 | override fun getReference(): RemotePlaylistRef = this 10 | 11 | override fun getEmptyData(): RemotePlaylistData = RemotePlaylistData(id) 12 | override fun createDbEntry(db: Database) { 13 | db.playlistQueries.insertById(id, null) 14 | } 15 | 16 | override val property_rememberer: PropertyRememberer = PropertyRememberer() 17 | } 18 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/song/SongRef.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.song 2 | 3 | import com.toasterofbread.spmp.model.mediaitem.MediaItemRef 4 | import com.toasterofbread.spmp.model.mediaitem.PropertyRememberer 5 | 6 | class SongRef(override val id: String): Song, MediaItemRef() { 7 | override fun toString(): String = "SongRef($id)" 8 | override val property_rememberer: PropertyRememberer = PropertyRememberer() 9 | override fun getReference(): SongRef = this 10 | } 11 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/mediaitem/toThumbnailProvider.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem 2 | 3 | import com.toasterofbread.spmp.db.mediaitem.ThumbnailProviderById 4 | import dev.toastbits.ytmkt.model.external.ThumbnailProvider 5 | import dev.toastbits.ytmkt.model.external.ThumbnailProviderImpl 6 | 7 | fun ThumbnailProviderById.toThumbnailProvider(): ThumbnailProvider? = 8 | if (thumb_url_a == null) null 9 | else ThumbnailProviderImpl(thumb_url_a, thumb_url_b) 10 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/radio/PlaylistItemsRadioContinuation.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.radio 2 | 3 | import com.toasterofbread.spmp.model.mediaitem.song.SongRef 4 | import dev.toastbits.ytmkt.endpoint.RadioBuilderModifier 5 | import dev.toastbits.ytmkt.model.YtmApi 6 | import dev.toastbits.ytmkt.model.external.mediaitem.YtmMediaItem 7 | import dev.toastbits.ytmkt.radio.RadioContinuation 8 | import kotlinx.serialization.Serializable 9 | 10 | @Serializable 11 | internal data class PlaylistItemsRadioContinuation( 12 | val song_ids: List, 13 | val head: Int, 14 | @Serializable(with = RadioContinuationSerializer::class) 15 | val next_continuation: RadioContinuation? 16 | ): RadioContinuation { 17 | override suspend fun loadContinuation( 18 | api: YtmApi, 19 | filters: List 20 | ): Result, RadioContinuation?>> = runCatching { 21 | val continuation: RadioContinuation? = 22 | if (song_ids.size - head > PLAYLIST_RADIO_LOAD_STEP_SIZE) copy(head = head + PLAYLIST_RADIO_LOAD_STEP_SIZE) 23 | else next_continuation 24 | 25 | return@runCatching ( 26 | song_ids.subList( 27 | fromIndex = head, 28 | toIndex = (head + PLAYLIST_RADIO_LOAD_STEP_SIZE).coerceAtMost(song_ids.size) 29 | ) 30 | .map { SongRef(it) } 31 | to continuation 32 | ) 33 | } 34 | 35 | companion object { 36 | const val PLAYLIST_RADIO_LOAD_STEP_SIZE: Int = 30 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/settings/SettingsGroup.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.settings 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import dev.toastbits.composekit.settings.ComposeKitSettingsGroup 6 | 7 | interface SettingsGroup: ComposeKitSettingsGroup { 8 | val hidden: Boolean get() = false 9 | 10 | @Composable 11 | fun titleBarEndContent(modifier: Modifier) {} 12 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/settings/SettingsGroupImpl.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.settings 2 | 3 | import dev.toastbits.composekit.settings.ComposeKitSettingsGroupImpl 4 | import dev.toastbits.composekit.settings.PlatformSettings 5 | 6 | abstract class SettingsGroupImpl( 7 | groupKey: String, 8 | settings: PlatformSettings 9 | ): ComposeKitSettingsGroupImpl(groupKey, settings), SettingsGroup -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/settings/category/DiscordAuthSettings.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.settings.category 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.vector.ImageVector 5 | import com.toasterofbread.spmp.ProjectBuildConfig 6 | import com.toasterofbread.spmp.model.settings.SettingsGroupImpl 7 | import com.toasterofbread.spmp.platform.AppContext 8 | import dev.toastbits.composekit.settingsitem.domain.PlatformSettingsProperty 9 | import dev.toastbits.composekit.settingsitem.domain.SettingsItem 10 | import org.jetbrains.compose.resources.stringResource 11 | import spmp.shared.generated.resources.Res 12 | import spmp.shared.generated.resources.s_cat_discord_auth 13 | 14 | class DiscordAuthSettings(val context: AppContext): SettingsGroupImpl("DISCORDAUTH", context.getPrefs()) { 15 | val DISCORD_ACCOUNT_TOKEN: PlatformSettingsProperty by property( 16 | getName = { "" }, 17 | getDescription = { null }, 18 | getDefaultValue = { ProjectBuildConfig.DISCORD_ACCOUNT_TOKEN ?: "" } 19 | ) 20 | val DISCORD_WARNING_ACCEPTED: PlatformSettingsProperty by property( 21 | getName = { "" }, 22 | getDescription = { null }, 23 | getDefaultValue = { false } 24 | ) 25 | 26 | @Composable 27 | override fun getTitle(): String = stringResource(Res.string.s_cat_discord_auth) 28 | 29 | @Composable 30 | override fun getDescription(): String = "" 31 | 32 | @Composable 33 | override fun getIcon(): ImageVector = DiscordSettings.getDiscordIcon() 34 | 35 | override fun getConfigurationItems(): List = emptyList() 36 | 37 | override val hidden: Boolean = true 38 | } 39 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/settings/category/SearchSettings.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.settings.category 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.Search 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | import com.toasterofbread.spmp.model.settings.SettingsGroupImpl 8 | import com.toasterofbread.spmp.platform.AppContext 9 | import com.toasterofbread.spmp.ui.layout.apppage.settingspage.category.getSearchCategoryItems 10 | import dev.toastbits.composekit.settingsitem.domain.PlatformSettingsProperty 11 | import dev.toastbits.composekit.settingsitem.domain.SettingsItem 12 | import org.jetbrains.compose.resources.stringResource 13 | import spmp.shared.generated.resources.Res 14 | import spmp.shared.generated.resources.s_key_search_search_for_non_music 15 | import spmp.shared.generated.resources.s_sub_search_search_for_non_music 16 | 17 | class SearchSettings(val context: AppContext): SettingsGroupImpl("SEARCH", context.getPrefs()) { 18 | val SEARCH_FOR_NON_MUSIC: PlatformSettingsProperty by property( 19 | getName = { stringResource(Res.string.s_key_search_search_for_non_music) }, 20 | getDescription = { stringResource(Res.string.s_sub_search_search_for_non_music) }, 21 | getDefaultValue = { false } 22 | ) 23 | 24 | @Composable 25 | override fun getTitle(): String = "" 26 | 27 | @Composable 28 | override fun getDescription(): String = "" 29 | 30 | @Composable 31 | override fun getIcon(): ImageVector = Icons.Outlined.Search 32 | 33 | override fun getConfigurationItems(): List = getSearchCategoryItems(context) 34 | 35 | override val hidden: Boolean = true 36 | } 37 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/settings/category/YTApiSettings.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.settings.category 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.PlayCircle 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | import com.toasterofbread.spmp.model.settings.SettingsGroupImpl 8 | import com.toasterofbread.spmp.youtubeapi.YtmApiType 9 | import dev.toastbits.composekit.settings.PlatformSettings 10 | import dev.toastbits.composekit.settingsitem.domain.PlatformSettingsProperty 11 | import dev.toastbits.composekit.settingsitem.domain.SettingsItem 12 | 13 | class YTApiSettings(prefs: PlatformSettings): SettingsGroupImpl("YTAPI", prefs) { 14 | val API_TYPE: PlatformSettingsProperty by enumProperty( 15 | getName = { "" }, 16 | getDescription = { null }, 17 | getDefaultValue = { YtmApiType.DEFAULT } 18 | ) 19 | val API_URL: PlatformSettingsProperty by property( 20 | getName = { "" }, 21 | getDescription = { null }, 22 | getDefaultValue = { YtmApiType.DEFAULT.getDefaultUrl() } 23 | ) 24 | 25 | @Composable 26 | override fun getTitle(): String = "" 27 | 28 | @Composable 29 | override fun getDescription(): String = "" 30 | 31 | @Composable 32 | override fun getIcon(): ImageVector = Icons.Outlined.PlayCircle 33 | 34 | override fun getConfigurationItems(): List = emptyList() 35 | 36 | override val hidden: Boolean = true 37 | } 38 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/FormFactor.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import com.toasterofbread.spmp.service.playercontroller.PlayerState 4 | import androidx.compose.runtime.* 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.runtime.derivedStateOf 8 | import androidx.compose.runtime.State 9 | import androidx.compose.ui.unit.dp 10 | import LocalPlayerState 11 | import androidx.compose.ui.unit.DpSize 12 | 13 | enum class FormFactor { 14 | PORTRAIT, 15 | LANDSCAPE; 16 | 17 | val is_large: Boolean get() = this != PORTRAIT 18 | 19 | companion object { 20 | private var form_factor_override: FormFactor? by mutableStateOf(null) 21 | 22 | fun setOverride(form_factor_override: FormFactor?) { 23 | this.form_factor_override = form_factor_override 24 | } 25 | 26 | fun getCurrent(screen_size: DpSize): FormFactor { 27 | form_factor_override?.also { 28 | return it 29 | } 30 | 31 | if (screen_size.width >= 500.dp) { 32 | return FormFactor.LANDSCAPE 33 | } 34 | 35 | return ( 36 | if (screen_size.width > screen_size.height) FormFactor.LANDSCAPE 37 | else FormFactor.PORTRAIT 38 | ) 39 | } 40 | 41 | fun getCurrent(player: PlayerState): FormFactor = getCurrent(player.screen_size) 42 | 43 | @Composable 44 | fun observe(min_portrait_ratio: Float? = null): State { 45 | val screen_size: DpSize by LocalPlayerState.current.screen_size_state 46 | return remember { derivedStateOf { 47 | form_factor_override ?: getCurrent(screen_size) 48 | } } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/ImageBitmap.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.ImageBitmap 5 | 6 | expect fun createImageBitmapUtil(): ImageBitmapUtil? 7 | 8 | expect fun ByteArray.toImageBitmap(): ImageBitmap 9 | expect fun ImageBitmap.toByteArray(): ByteArray 10 | expect fun ImageBitmap.crop(x: Int, y: Int, width: Int, height: Int): ImageBitmap 11 | expect fun ImageBitmap.getPixel(x: Int, y: Int): Color 12 | 13 | interface ImageBitmapUtil { 14 | fun scale(image: ImageBitmap, width: Int, height: Int): ImageBitmap 15 | fun generatePalette(image: ImageBitmap, max_amount: Int): List 16 | } 17 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/PlatformService.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | expect abstract class PlatformBinder() 4 | 5 | interface PlatformService { 6 | val context: AppContext 7 | 8 | fun onCreate() 9 | fun onDestroy() 10 | 11 | fun onBind(): PlatformBinder? 12 | 13 | fun sendMessageOut(data: Any?) 14 | fun onMessage(data: Any?) 15 | 16 | fun addMessageReceiver(receiver: (Any?) -> Unit) 17 | fun removeMessageReceiver(receiver: (Any?) -> Unit) 18 | } 19 | 20 | expect open class PlatformServiceImpl(): PlatformService { 21 | override val context: AppContext 22 | 23 | override fun onCreate() 24 | override fun onDestroy() 25 | 26 | override fun onBind(): PlatformBinder? 27 | 28 | override fun sendMessageOut(data: Any?) 29 | override fun onMessage(data: Any?) 30 | 31 | override fun addMessageReceiver(receiver: (Any?) -> Unit) 32 | override fun removeMessageReceiver(receiver: (Any?) -> Unit) 33 | } 34 | 35 | expect inline fun startPlatformService( 36 | context: AppContext, 37 | createInstance: () -> T, 38 | crossinline onConnected: (binder: PlatformBinder?) -> Unit = {}, 39 | crossinline onDisconnected: () -> Unit = {} 40 | ): Any // Service connection 41 | 42 | expect fun unbindPlatformService(context: AppContext, connection: Any) 43 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import com.toasterofbread.spmp.model.mediaitem.song.Song 4 | import dev.toastbits.spms.socketapi.shared.SpMsPlayerRepeatMode 5 | import dev.toastbits.spms.socketapi.shared.SpMsPlayerState 6 | 7 | expect abstract class PlayerListener() { 8 | open fun onSongTransition(song: Song?, manual: Boolean) 9 | open fun onStateChanged(state: SpMsPlayerState) 10 | open fun onPlayingChanged(is_playing: Boolean) 11 | open fun onRepeatModeChanged(repeat_mode: SpMsPlayerRepeatMode) 12 | open fun onVolumeChanged(volume: Float) 13 | open fun onDurationChanged(duration_ms: Long) 14 | open fun onSeeked(position_ms: Long) 15 | open fun onUndoStateChanged() 16 | 17 | open fun onSongAdded(index: Int, song: Song) 18 | open fun onSongRemoved(index: Int, song: Song) 19 | open fun onSongMoved(from: Int, to: Int) 20 | 21 | open fun onEvents() 22 | } 23 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/ProjectJson.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | object ProjectJson { 6 | val instance: Json = 7 | Json { 8 | ignoreUnknownKeys = true 9 | explicitNulls = false 10 | useArrayPolymorphism = true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/SqlDriver.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import app.cash.sqldelight.db.SqlDriver 4 | import com.toasterofbread.spmp.db.Database 5 | import com.toasterofbread.spmp.resources.migrations.Migration 6 | 7 | expect fun AppContext.getSqlDriver(): SqlDriver 8 | 9 | private var instance: Database? = null 10 | 11 | fun AppContext.createDatabase(): Database { 12 | if (instance == null) { 13 | val driver: SqlDriver = getSqlDriver() 14 | Migration.updateDriverIfNeeded(driver) 15 | instance = Database(driver) 16 | } 17 | return instance!! 18 | } 19 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/WebViewLogin.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import androidx.compose.ui.Modifier 4 | import androidx.compose.runtime.Composable 5 | 6 | expect fun isWebViewLoginSupported(): Boolean 7 | 8 | data class WebViewRequest( 9 | val url: String, 10 | val is_redirect: Boolean, 11 | val method: String, 12 | val headers: Map 13 | ) 14 | 15 | // Returns true if restart required 16 | internal expect suspend fun initWebViewLogin(context: AppContext, onProgress: (Float, String?) -> Unit): Result 17 | 18 | @Composable 19 | expect fun WebViewLogin( 20 | initial_url: String, 21 | onClosed: () -> Unit, 22 | shouldShowPage: (url: String) -> Boolean, 23 | modifier: Modifier = Modifier, 24 | loading_message: String? = null, 25 | base_cookies: String = "", 26 | user_agent: String? = null, 27 | viewport_width: String? = null, 28 | viewport_height: String? = null, 29 | onRequestIntercepted: suspend (WebViewRequest, openUrl: (String) -> Unit, getCookies: suspend (String) -> List>) -> Unit 30 | ) 31 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/ClientServerPlayerService.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice 2 | 3 | import com.toasterofbread.spmp.model.mediaitem.song.Song 4 | import com.toasterofbread.spmp.platform.download.DownloadStatus 5 | import io.ktor.http.Headers 6 | import dev.toastbits.spms.socketapi.shared.SpMsClientInfo 7 | 8 | interface ClientServerPlayerService: PlayerService { 9 | data class ServerInfo( 10 | val ip: String, 11 | val port: Int, 12 | val protocol: String, 13 | val name: String, 14 | val device_name: String, 15 | val machine_id: String, 16 | val spms_api_version: Int 17 | ) 18 | 19 | val connected_server: ServerInfo? 20 | 21 | suspend fun getPeers(): Result> 22 | suspend fun sendAuthInfoToPlayers(ytm_auth: Pair?): Result 23 | 24 | fun onSongFilesAdded(songs: List) 25 | fun onSongFilesDeleted(songs: List) 26 | fun onLocalSongsSynced(songs: Iterable) 27 | } 28 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/ForwardingPlayerService.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice 2 | 3 | open class ForwardingPlayerService( 4 | protected val player: PlayerService 5 | ): PlayerService by player 6 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/LocalServer.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice 2 | 3 | import com.toasterofbread.spmp.platform.AppContext 4 | import kotlinx.coroutines.Job 5 | 6 | expect object LocalServer { 7 | fun isAvailable(): Boolean 8 | 9 | suspend fun getLocalServerUnavailabilityReason(): String? 10 | 11 | suspend fun startLocalServer( 12 | context: AppContext, 13 | port: Int 14 | ): Result 15 | } 16 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformExternalPlayerService.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice 2 | 3 | expect class PlatformExternalPlayerService(): PlayerService { 4 | companion object: PlayerServiceCompanion 5 | } 6 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlayerServiceCompanion.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice 2 | 3 | import ProgramArguments 4 | import androidx.compose.runtime.Composable 5 | import com.toasterofbread.spmp.platform.AppContext 6 | 7 | interface PlayerServiceCompanion { 8 | suspend fun getUnavailabilityReason(context: AppContext, launch_arguments: ProgramArguments): String? = null 9 | fun isAvailable(context: AppContext, launch_arguments: ProgramArguments): Boolean 10 | 11 | fun isServiceRunning(context: AppContext): Boolean 12 | fun isServiceAttached(context: AppContext): Boolean = false 13 | fun playsAudio(): Boolean = false 14 | 15 | suspend fun connect( 16 | context: AppContext, 17 | launch_arguments: ProgramArguments, 18 | instance: PlayerService? = null, 19 | onConnected: (PlayerService) -> Unit, 20 | onDisconnected: () -> Unit 21 | ): Any 22 | 23 | fun disconnect(context: AppContext, connection: Any) 24 | } 25 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/visualiser/MusicVisualiser.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.visualiser 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.Modifier 6 | 7 | expect class MusicVisualiser { 8 | @Composable 9 | fun Visualiser(colour: Color, modifier: Modifier, opacity: Float = 1f) 10 | 11 | companion object { 12 | fun isSupported(): Boolean 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/resources/migrations/1.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.resources.migrations 2 | 3 | import app.cash.sqldelight.db.SqlDriver 4 | import com.toasterofbread.spmp.resources.migrations.Migration.performMigration 5 | 6 | internal fun SqlDriver.migrateToVersion1() = performMigration( 7 | mapOf( 8 | "Playlist" to listOf( 9 | Migration.AddColumn("playlist_url", "TEXT") 10 | ), 11 | "SongFeedRow" to listOf( 12 | Migration.AddColumn("layout_type", "INTEGER") 13 | ) 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/resources/migrations/2.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.resources.migrations 2 | 3 | import app.cash.sqldelight.db.SqlDriver 4 | import com.toasterofbread.spmp.resources.migrations.Migration.performMigration 5 | 6 | internal fun SqlDriver.migrateToVersion2() = performMigration( 7 | mapOf( 8 | "Song" to listOf( 9 | Migration.AddColumn("loudness_db", "REAL"), 10 | Migration.AddColumn("explicit", "INTEGER"), 11 | Migration.AddColumn("background_image_opacity", "REAL"), 12 | Migration.AddColumn("image_shadow_radius", "REAL") 13 | ), 14 | "Artist" to listOf( 15 | Migration.AddColumn("shuffle_playlist_id", "TEXT") 16 | ) 17 | ) 18 | ) 19 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/resources/migrations/5.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.resources.migrations 2 | 3 | import app.cash.sqldelight.db.SqlDriver 4 | import com.toasterofbread.spmp.resources.migrations.Migration.performMigration 5 | 6 | internal fun SqlDriver.migrateToVersion5() = performMigration( 7 | mapOf( 8 | "Song" to listOf( 9 | Migration.AddColumn("background_wave_speed", "REAL"), 10 | Migration.AddColumn("background_wave_opacity", "REAL") 11 | ) 12 | ) 13 | ) 14 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/resources/migrations/6.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.resources.migrations 2 | 3 | import app.cash.sqldelight.db.SqlDriver 4 | import com.toasterofbread.spmp.resources.migrations.Migration.performMigration 5 | 6 | internal fun SqlDriver.migrateToVersion6() = performMigration( 7 | mapOf( 8 | "Playlist" to listOf( 9 | Migration.AddColumn("owned_by_user", "INTEGER"), 10 | Migration.AddColumn("artists", "TEXT") 11 | ), 12 | "Song" to listOf( 13 | Migration.AddColumn("video_position", "INTEGER"), 14 | Migration.AddColumn("landscape_queue_opacity", "REAL"), 15 | Migration.AddColumn("artists", "TEXT") 16 | ) 17 | ) 18 | ) 19 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/resources/migrations/7.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.resources.migrations 2 | 3 | import app.cash.sqldelight.db.SqlDriver 4 | import com.toasterofbread.spmp.resources.migrations.Migration.performMigration 5 | 6 | internal fun SqlDriver.migrateToVersion7() = performMigration( 7 | mapOf( 8 | ) 9 | ) 10 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/resources/migrations/8.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.resources.migrations 2 | 3 | import app.cash.sqldelight.db.SqlDriver 4 | import com.toasterofbread.spmp.resources.migrations.Migration.performMigration 5 | 6 | internal fun SqlDriver.migrateToVersion8() = performMigration( 7 | mapOf( 8 | "PersistentQueueItem" to listOf( 9 | Migration.CreateTable( 10 | """ 11 | item_index INTEGER NOT NULL PRIMARY KEY, 12 | id TEXT NOT NULL, 13 | 14 | FOREIGN KEY (id) REFERENCES MediaItem(id) 15 | """.trimIndent() 16 | ) 17 | ), 18 | "PersistentQueueMetadata" to listOf( 19 | Migration.CreateTable( 20 | """ 21 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 22 | queue_index INTEGER NOT NULL, 23 | playback_position_ms INTEGER NOT NULL 24 | """.trimIndent() 25 | ) 26 | ) 27 | ) 28 | ) 29 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/resources/migrations/9.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.resources.migrations 2 | 3 | import app.cash.sqldelight.db.SqlDriver 4 | import com.toasterofbread.spmp.resources.migrations.Migration.performMigration 5 | 6 | internal fun SqlDriver.migrateToVersion9() = performMigration( 7 | mapOf( 8 | "AndroidWidget" to listOf( 9 | Migration.CreateTable( 10 | """ 11 | id INTEGER NOT NULL PRIMARY KEY, 12 | configuration TEXT NOT NULL 13 | """.trimIndent() 14 | ) 15 | ) 16 | ) 17 | ) 18 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/EndpointNotImplementedMessage.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.component 2 | 3 | import dev.toastbits.ytmkt.model.ApiImplementable 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | 10 | @Composable 11 | fun ApiImplementable.NotImplementedMessage(modifier: Modifier) { 12 | Box(modifier, contentAlignment = Alignment.Center) { 13 | Text(getNotImplementedMessage()) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/longpressmenu/LongPressMenu.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.component.longpressmenu 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.foundation.shape.RoundedCornerShape 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.draw.clip 9 | import androidx.compose.ui.draw.scale 10 | import dev.toastbits.composekit.util.platform.Platform 11 | import com.toasterofbread.spmp.ui.layout.nowplaying.overlay.songtheme.DEFAULT_THUMBNAIL_ROUNDING 12 | 13 | private const val LONG_PRESS_ICON_INDICATION_SCALE: Float = 0.4f 14 | 15 | @Composable 16 | fun Modifier.longPressMenuIcon(data: LongPressMenuData, enabled: Boolean = true): Modifier { 17 | val scale by animateFloatAsState(1f + (if (!enabled) 0f else data.getInteractionHintScale() * LONG_PRESS_ICON_INDICATION_SCALE)) 18 | return this 19 | .clip(data.thumb_shape ?: RoundedCornerShape(DEFAULT_THUMBNAIL_ROUNDING)) 20 | .scale(scale) 21 | } 22 | 23 | @Composable 24 | fun LongPressMenu( 25 | showing: Boolean, 26 | onDismissRequest: () -> Unit, 27 | data: LongPressMenuData 28 | ) { 29 | when (Platform.current) { 30 | Platform.ANDROID -> AndroidLongPressMenu(showing, onDismissRequest, data) 31 | Platform.DESKTOP, 32 | Platform.WEB -> DesktopLongPressMenu(showing, onDismissRequest, data) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/mediaitempreview/ThumbShapes.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.component.mediaitempreview 2 | 3 | import SpMp 4 | import androidx.compose.foundation.shape.RoundedCornerShape 5 | import androidx.compose.ui.graphics.Shape 6 | import androidx.compose.ui.unit.dp 7 | import com.toasterofbread.spmp.model.mediaitem.enums.MediaItemType 8 | import com.toasterofbread.spmp.platform.FormFactor 9 | 10 | fun getSongThumbShape(): Shape = 11 | if (FormFactor.getCurrent(SpMp.player_state).is_large) RoundedCornerShape(4.dp) 12 | else RoundedCornerShape(10.dp) 13 | 14 | fun getArtistThumbShape(): Shape = RoundedCornerShape(50) 15 | 16 | fun getPlaylistThumbShape(): Shape = RoundedCornerShape(10.dp) 17 | 18 | fun MediaItemType.getThumbShape(): Shape = 19 | when (this) { 20 | MediaItemType.SONG -> getSongThumbShape() 21 | MediaItemType.ARTIST -> getArtistThumbShape() 22 | MediaItemType.PLAYLIST_REM, MediaItemType.PLAYLIST_LOC -> getPlaylistThumbShape() 23 | } 24 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/GenericFeedViewMoreAppPage.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.apppage 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import com.toasterofbread.spmp.ui.component.multiselect.MediaItemMultiSelectContext 8 | import com.toasterofbread.spmp.ui.layout.GenericFeedViewMorePage 9 | 10 | data class GenericFeedViewMoreAppPage( 11 | override val state: AppPageState, 12 | private val browse_id: String, 13 | private val title: String?, 14 | ): AppPage() { 15 | @Composable 16 | override fun ColumnScope.Page( 17 | multiselect_context: MediaItemMultiSelectContext, 18 | modifier: Modifier, 19 | content_padding: PaddingValues, 20 | close: () -> Unit, 21 | ) { 22 | GenericFeedViewMorePage(browse_id, modifier, content_padding = content_padding, title = title) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/RadioBuilderAppPage.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.apppage 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import com.toasterofbread.spmp.ui.component.multiselect.MediaItemMultiSelectContext 9 | import com.toasterofbread.spmp.ui.layout.radiobuilder.RadioBuilderPage 10 | 11 | class RadioBuilderAppPage(override val state: AppPageState): AppPage() { 12 | @Composable 13 | override fun ColumnScope.Page( 14 | multiselect_context: MediaItemMultiSelectContext, 15 | modifier: Modifier, 16 | content_padding: PaddingValues, 17 | close: () -> Unit, 18 | ) { 19 | RadioBuilderPage( 20 | content_padding, 21 | Modifier.fillMaxSize(), 22 | close 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/SongAppPage.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.apppage 2 | 3 | import com.toasterofbread.spmp.ui.layout.apppage.AppPageState 4 | import com.toasterofbread.spmp.ui.layout.apppage.AppPageWithItem 5 | import com.toasterofbread.spmp.ui.layout.SongRelatedPage 6 | import com.toasterofbread.spmp.ui.component.multiselect.MediaItemMultiSelectContext 7 | import com.toasterofbread.spmp.model.mediaitem.song.Song 8 | import com.toasterofbread.spmp.model.mediaitem.MediaItemHolder 9 | import dev.toastbits.ytmkt.model.external.YoutubePage 10 | import androidx.compose.foundation.layout.ColumnScope 11 | import androidx.compose.foundation.layout.PaddingValues 12 | import androidx.compose.runtime.* 13 | import androidx.compose.ui.Modifier 14 | 15 | data class SongAppPage( 16 | override val state: AppPageState, 17 | override val item: Song, 18 | private val browse_params: YoutubePage.BrowseParamsData? = null 19 | ): AppPageWithItem() { 20 | private var previous_item: MediaItemHolder? by mutableStateOf(null) 21 | 22 | override fun onOpened(from_item: MediaItemHolder?) { 23 | super.onOpened(from_item) 24 | previous_item = from_item 25 | } 26 | 27 | @Composable 28 | override fun ColumnScope.Page( 29 | multiselect_context: MediaItemMultiSelectContext, 30 | modifier: Modifier, 31 | content_padding: PaddingValues, 32 | close: () -> Unit, 33 | ) { 34 | SongRelatedPage( 35 | item, 36 | state.context.ytapi.SongRelatedContent, 37 | modifier, 38 | previous_item?.item, 39 | content_padding, 40 | close = close 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/searchpage/HorizontalSearchPagePrimaryBar.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.apppage.searchpage 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.unit.dp 6 | import androidx.compose.foundation.layout.PaddingValues 7 | import androidx.compose.foundation.layout.height 8 | import com.toasterofbread.spmp.ui.layout.contentbar.layoutslot.LayoutSlot 9 | 10 | @Composable 11 | internal fun SearchAppPage.HorizontalSearchPrimaryBar( 12 | slot: LayoutSlot, 13 | modifier: Modifier, 14 | content_padding: PaddingValues, 15 | lazy: Boolean 16 | ) { 17 | SearchFiltersRow(modifier.height(60.dp), content_padding, lazy) 18 | } 19 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/settingspage/AppSliderItem.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.apppage.settingspage 2 | 3 | import androidx.compose.ui.Modifier 4 | import dev.toastbits.composekit.util.roundTo 5 | import com.toasterofbread.spmp.ui.layout.apppage.mainpage.appTextField 6 | import dev.toastbits.composekit.settingsitem.domain.PlatformSettingsProperty 7 | import dev.toastbits.composekit.settingsitem.presentation.ui.component.item.SliderSettingsItem 8 | import dev.toastbits.composekit.util.CustomStringResource 9 | import dev.toastbits.composekit.util.toCustomResource 10 | import org.jetbrains.compose.resources.getString 11 | import spmp.shared.generated.resources.Res 12 | import spmp.shared.generated.resources.settings_value_not_int 13 | import spmp.shared.generated.resources.settings_value_not_float 14 | import spmp.shared.generated.resources.`settings_value_out_of_$range` 15 | 16 | fun AppSliderItem( 17 | state: PlatformSettingsProperty, 18 | min_label: CustomStringResource? = null, 19 | max_label: CustomStringResource? = null, 20 | steps: Int = 0, 21 | range: ClosedFloatingPointRange = 0f .. 1f, 22 | getValueText: ((value: Number) -> String?)? = { 23 | if (it is Float) it.roundTo(2).toString() 24 | else it.toString() 25 | } 26 | ): SliderSettingsItem = 27 | SliderSettingsItem( 28 | state = state, 29 | minLabel = min_label, 30 | maxLabel = max_label, 31 | steps = steps, 32 | range = range, 33 | getValueText = getValueText, 34 | getFieldModifier = { Modifier.appTextField() } 35 | ) 36 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/settingspage/AppStringSetItem.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.apppage.settingspage 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.unit.Dp 6 | import androidx.compose.ui.unit.dp 7 | import com.toasterofbread.spmp.ui.layout.apppage.mainpage.appTextField 8 | import dev.toastbits.composekit.settingsitem.domain.PlatformSettingsProperty 9 | import dev.toastbits.composekit.settingsitem.presentation.ui.component.item.StringSetSettingsItem 10 | import org.jetbrains.compose.resources.StringResource 11 | import spmp.shared.generated.resources.Res 12 | import spmp.shared.generated.resources.settings_string_set_item_already_added 13 | import spmp.shared.generated.resources.settings_string_set_item_empty 14 | 15 | fun AppStringSetItem( 16 | state: PlatformSettingsProperty>, 17 | add_dialog_title: StringResource, 18 | single_line_content: Boolean = true, 19 | height: Dp = 300.dp, 20 | itemToText: @Composable (String) -> String = { it }, 21 | textToItem: (String) -> String = { it } 22 | ): StringSetSettingsItem = 23 | StringSetSettingsItem( 24 | state = state, 25 | add_dialog_title = add_dialog_title, 26 | msg_item_already_added = Res.string.settings_string_set_item_already_added, 27 | msg_set_empty = Res.string.settings_string_set_item_empty, 28 | single_line_content = single_line_content, 29 | height = height, 30 | itemToText = itemToText, 31 | textToItem = textToItem, 32 | getFieldModifier = { Modifier.appTextField() } 33 | ) 34 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/settingspage/category/FilterCategory.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.apppage.settingspage.category 2 | 3 | import dev.toastbits.composekit.settingsitem.domain.SettingsItem 4 | import dev.toastbits.composekit.settingsitem.presentation.ui.component.item.ToggleSettingsItem 5 | import com.toasterofbread.spmp.ui.layout.apppage.settingspage.AppStringSetItem 6 | import com.toasterofbread.spmp.platform.AppContext 7 | import spmp.shared.generated.resources.Res 8 | import spmp.shared.generated.resources.s_filter_title_keywords_dialog_title 9 | 10 | internal fun getFilterCategoryItems(context: AppContext): List { 11 | return listOf( 12 | ToggleSettingsItem( 13 | context.settings.Filter.ENABLE 14 | ), 15 | 16 | ToggleSettingsItem( 17 | context.settings.Filter.APPLY_TO_PLAYLIST_ITEMS 18 | ), 19 | 20 | ToggleSettingsItem( 21 | context.settings.Filter.APPLY_TO_ARTISTS 22 | ), 23 | 24 | ToggleSettingsItem( 25 | context.settings.Filter.APPLY_TO_ARTIST_ITEMS 26 | ), 27 | 28 | AppStringSetItem( 29 | context.settings.Filter.TITLE_KEYWORDS, 30 | Res.string.s_filter_title_keywords_dialog_title 31 | ) 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/settingspage/category/LayoutCategory.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.apppage.settingspage.category 2 | 3 | import com.toasterofbread.spmp.ui.layout.contentbar.layoutslot.getLayoutSlotEditorSettingsItems 4 | import com.toasterofbread.spmp.platform.AppContext 5 | import dev.toastbits.composekit.settingsitem.domain.SettingsItem 6 | 7 | internal fun getLayoutCategoryItems(context: AppContext): List = getLayoutSlotEditorSettingsItems(context) 8 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/settingspage/category/SearchCategory.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.apppage.settingspage.category 2 | 3 | import dev.toastbits.composekit.settingsitem.presentation.ui.component.item.ToggleSettingsItem 4 | import dev.toastbits.composekit.settingsitem.domain.SettingsItem 5 | import com.toasterofbread.spmp.platform.AppContext 6 | 7 | internal fun getSearchCategoryItems(context: AppContext): List = 8 | listOf( 9 | ToggleSettingsItem( 10 | context.settings.Search.SEARCH_FOR_NON_MUSIC 11 | ) 12 | ) 13 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/settingspage/category/ShortcutCategory.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.apppage.settingspage.category 2 | 3 | import dev.toastbits.composekit.settingsitem.domain.SettingsItem 4 | import dev.toastbits.composekit.settingsitem.presentation.ui.component.item.ComposableSettingsItem 5 | import com.toasterofbread.spmp.model.appaction.shortcut.ShortcutsEditor 6 | import com.toasterofbread.spmp.platform.AppContext 7 | 8 | internal fun getShortcutCategoryItems(context: AppContext): List = 9 | listOf( 10 | ComposableSettingsItem( 11 | listOf( 12 | context.settings.Shortcut.CONFIGURED_SHORTCUTS, 13 | context.settings.Shortcut.NAVIGATE_SONG_WITH_NUMBERS 14 | ), 15 | resetComposeUiState = {} 16 | ) { modifier -> 17 | ShortcutsEditor(modifier) 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/settingspage/category/YoutubeAccountCategory.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.apppage.settingspage.category 2 | 3 | import dev.toastbits.composekit.settingsitem.domain.SettingsItem 4 | import dev.toastbits.composekit.settingsitem.presentation.ui.component.item.ToggleSettingsItem 5 | import com.toasterofbread.spmp.platform.AppContext 6 | 7 | internal fun getYoutubeAccountCategory(context: AppContext): List = 8 | listOf( 9 | ToggleSettingsItem( 10 | context.settings.Misc.ADD_SONGS_TO_HISTORY 11 | ) 12 | ) 13 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/songfeedpage/SongFeedPageLoadingView.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.apppage.songfeedpage 2 | 3 | import LocalPlayerState 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | import dev.toastbits.composekit.components.utils.composable.SubtleLoadingIndicator 9 | import org.jetbrains.compose.resources.stringResource 10 | import spmp.shared.generated.resources.Res 11 | import spmp.shared.generated.resources.loading_feed 12 | 13 | @Composable 14 | internal fun SongFeedPageLoadingView(modifier: Modifier = Modifier) { 15 | val player = LocalPlayerState.current 16 | Box(modifier, contentAlignment = Alignment.Center) { 17 | SubtleLoadingIndicator(message = stringResource(Res.string.loading_feed), getColour = { player.theme.onBackground }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/artistpage/ArtistPageGetAllItems.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.artistpage 2 | 3 | import com.toasterofbread.spmp.model.mediaitem.MediaItem 4 | import com.toasterofbread.spmp.model.mediaitem.artist.ArtistLayout 5 | import dev.toastbits.ytmkt.model.external.mediaitem.MediaItemLayout 6 | import com.toasterofbread.spmp.ui.component.multiselect.MultiSelectItem 7 | import com.toasterofbread.spmp.service.playercontroller.PlayerState 8 | import dev.toastbits.ytmkt.endpoint.ArtistWithParamsRow 9 | import dev.toastbits.ytmkt.model.external.ItemLayoutType 10 | 11 | internal fun artistPageGetAllItems(player: PlayerState, browse_params_rows: List?, item_layouts: List?): List> { 12 | if (browse_params_rows == null) { 13 | return item_layouts.orEmpty().mapNotNull { layout -> 14 | layout.Items.get(player.database)?.map { 15 | Pair(it, null) 16 | } 17 | } 18 | } 19 | else { 20 | val row: ArtistWithParamsRow? = browse_params_rows.firstOrNull() 21 | return ( 22 | listOfNotNull( 23 | row?.items?.map { Pair(it, null) } 24 | ) 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/artistpage/DescriptionCard.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.artistpage 2 | 3 | import LocalPlayerState 4 | import androidx.compose.animation.animateContentSize 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.CardDefaults 8 | import androidx.compose.material3.ElevatedCard 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import com.toasterofbread.spmp.service.playercontroller.PlayerState 14 | import dev.toastbits.composekit.util.blendWith 15 | import dev.toastbits.composekit.components.utils.composable.LinkifyText 16 | 17 | @Composable 18 | fun DescriptionCard(description_text: String) { 19 | val player: PlayerState = LocalPlayerState.current 20 | 21 | ElevatedCard( 22 | Modifier 23 | .fillMaxWidth() 24 | .animateContentSize(), 25 | colors = CardDefaults.elevatedCardColors( 26 | containerColor = player.theme.accent.blendWith(player.theme.background, 0.05f) 27 | ) 28 | ) { 29 | LinkifyText( 30 | description_text, 31 | modifier = Modifier.padding(10.dp), 32 | highlight_colour = player.theme.onBackground, 33 | style = MaterialTheme.typography.bodyMedium.copy( 34 | color = player.theme.onBackground.copy(alpha = 0.8f) 35 | ) 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/contentbar/CircularReferenceWarning.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.contentbar 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.material3.Text 6 | import androidx.compose.material3.AlertDialog 7 | import androidx.compose.material3.Button 8 | import org.jetbrains.compose.resources.stringResource 9 | import spmp.shared.generated.resources.Res 10 | import spmp.shared.generated.resources.action_close 11 | import spmp.shared.generated.resources.content_bar_warn_circular_reference 12 | import spmp.shared.generated.resources.content_bar_warn_circular_reference_change_reverted 13 | 14 | @Composable 15 | fun CircularReferenceWarning(modifier: Modifier = Modifier, onDismiss: () -> Unit) { 16 | AlertDialog( 17 | onDismissRequest = onDismiss, 18 | confirmButton = { 19 | Button(onDismiss) { 20 | Text(stringResource(Res.string.action_close)) 21 | } 22 | }, 23 | title = { Text(stringResource(Res.string.content_bar_warn_circular_reference)) }, 24 | text = { Text(stringResource(Res.string.content_bar_warn_circular_reference_change_reverted)) } 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/contentbar/ContentBarReference.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.contentbar 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.Json 5 | import com.toasterofbread.spmp.ui.layout.contentbar.InternalContentBar 6 | import com.toasterofbread.spmp.platform.AppContext 7 | 8 | @Serializable 9 | data class ContentBarReference(val type: Type, val index: Int) { 10 | enum class Type { 11 | INTERNAL, 12 | CUSTOM, 13 | TEMPLATE 14 | } 15 | 16 | fun getBar(custom_bars: List): ContentBar? = 17 | when (type) { 18 | Type.INTERNAL -> InternalContentBar.ALL.getOrNull(index) 19 | Type.CUSTOM -> custom_bars.getOrNull(index) 20 | Type.TEMPLATE -> CustomContentBarTemplate.entries[index].getContentBar() 21 | } 22 | 23 | companion object { 24 | fun ofInternalBar(internal_bar: InternalContentBar): ContentBarReference = 25 | ContentBarReference(Type.INTERNAL, internal_bar.index) 26 | 27 | fun ofCustomBar(bar_index: Int): ContentBarReference = 28 | ContentBarReference(Type.CUSTOM, bar_index) 29 | 30 | fun ofTemplate(template: CustomContentBarTemplate): ContentBarReference = 31 | ContentBarReference(Type.TEMPLATE, template.ordinal) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/contentbar/TemplateCustomContentBar.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.contentbar 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | import androidx.compose.ui.unit.Dp 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.foundation.layout.PaddingValues 7 | import androidx.compose.runtime.* 8 | import com.toasterofbread.spmp.ui.layout.contentbar.layoutslot.LayoutSlot 9 | import com.toasterofbread.spmp.ui.layout.contentbar.element.ContentBarElement 10 | import dev.toastbits.composekit.theme.core.ThemeValues 11 | 12 | data class TemplateCustomContentBar( 13 | val template: CustomContentBarTemplate 14 | ): ContentBar() { 15 | @Composable 16 | override fun getName(): String = template.getName() 17 | @Composable 18 | override fun getDescription(): String? = template.getDescription() 19 | override fun getIcon(): ImageVector = template.getIcon() 20 | 21 | @Composable 22 | override fun isDisplaying(): Boolean { 23 | val elements: List = remember { template.getElements() } 24 | return elements.shouldDisplayBarOf() 25 | } 26 | 27 | @Composable 28 | override fun BarContent( 29 | slot: LayoutSlot, 30 | background_colour: ThemeValues.Slot?, 31 | content_padding: PaddingValues, 32 | distance_to_page: Dp, 33 | lazy: Boolean, 34 | modifier: Modifier 35 | ): Boolean { 36 | val elements: List = remember { template.getElements() } 37 | return CustomBarContent(elements, template.getDefaultHeight(), slot.is_vertical, content_padding, slot, background_colour, modifier) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/contentbar/element/ContentBarElementPinnedItems.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.contentbar.element 2 | 3 | import androidx.compose.runtime.* 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.unit.dp 6 | import androidx.compose.ui.unit.DpSize 7 | import androidx.compose.foundation.layout.BoxWithConstraints 8 | import androidx.compose.foundation.layout.padding 9 | import com.toasterofbread.spmp.ui.component.PinnedItemsList 10 | import com.toasterofbread.spmp.ui.layout.contentbar.layoutslot.LayoutSlot 11 | import kotlinx.serialization.Serializable 12 | 13 | @Serializable 14 | data class ContentBarElementPinnedItems( 15 | override val config: ContentBarElementConfig = ContentBarElementConfig() 16 | ): ContentBarElement() { 17 | override fun getType(): ContentBarElement.Type = ContentBarElement.Type.PINNED_ITEMS 18 | 19 | override fun copyWithConfig(config: ContentBarElementConfig): ContentBarElement = 20 | copy(config = config) 21 | 22 | override fun blocksIndicatorAnimation(): Boolean = true 23 | 24 | @Composable 25 | override fun ElementContent(vertical: Boolean, slot: LayoutSlot?, bar_size: DpSize, onPreviewClick: (() -> Unit)?, modifier: Modifier) { 26 | PinnedItemsList( 27 | vertical, 28 | modifier 29 | .run { 30 | if (vertical) padding(vertical = 10.dp) 31 | else padding(horizontal = 10.dp) 32 | }, 33 | onClick = onPreviewClick, 34 | scrolling = false 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/contentbar/layoutslot/ColourSource.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.contentbar.layoutslot 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.toArgb 5 | import androidx.compose.runtime.State 6 | import com.toasterofbread.spmp.service.playercontroller.PlayerState 7 | import androidx.compose.runtime.* 8 | import kotlinx.serialization.Serializable 9 | import LocalPlayerState 10 | import com.toasterofbread.spmp.ui.layout.nowplaying.getNPBackground 11 | import dev.toastbits.composekit.theme.core.ThemeValues 12 | import dev.toastbits.composekit.theme.core.get 13 | 14 | @Serializable 15 | sealed interface ColourSource { 16 | fun get(player: PlayerState): Color 17 | val theme_colour: ThemeValues.Slot? get() = null 18 | } 19 | 20 | @Serializable 21 | internal data class ThemeColourSource(override val theme_colour: ThemeValues.Slot): ColourSource { 22 | override fun get(player: PlayerState): Color = player.theme[theme_colour] 23 | } 24 | 25 | @Serializable 26 | internal class PlayerBackgroundColourSource: ColourSource { 27 | override fun get(player: PlayerState): Color = player.getNPBackground() 28 | } 29 | 30 | @Serializable 31 | data class CustomColourSource(val colour: Int): ColourSource { 32 | constructor(colour: Color): this(colour.toArgb()) 33 | 34 | override fun get(player: PlayerState): Color = Color(colour) 35 | } 36 | 37 | @Composable 38 | internal fun LayoutSlot.rememberColourSource(): State { 39 | val player: PlayerState = LocalPlayerState.current 40 | val colours: Map by player.settings.Layout.SLOT_COLOURS.observe() 41 | 42 | return remember { derivedStateOf { 43 | colours[getKey()] ?: getDefaultBackgroundColour(player.theme) 44 | } } 45 | } 46 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/NowPlayingTopOffsetSection.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.nowplaying 2 | 3 | enum class NowPlayingTopOffsetSection { 4 | MULTISELECT, 5 | PILL_MENU, 6 | SEARCH_PAGE_SUGGESTIONS, 7 | SEARCH_PAGE_BAR, 8 | PAGE_BAR, 9 | LAYOUT_SLOT; 10 | 11 | fun shouldIgnoreSection(other: NowPlayingTopOffsetSection): Boolean = 12 | (other == this && isMerged()) 13 | || when (this) { 14 | MULTISELECT -> other == SEARCH_PAGE_SUGGESTIONS 15 | else -> false 16 | } 17 | 18 | fun isMerged(): Boolean = 19 | when (this) { 20 | SEARCH_PAGE_SUGGESTIONS -> true 21 | SEARCH_PAGE_BAR -> true 22 | LAYOUT_SLOT -> true 23 | else -> false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/container/MinimisedProgressBar.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.nowplaying.container 2 | 3 | import LocalNowPlayingExpansion 4 | import LocalPlayerState 5 | import androidx.compose.material3.LinearProgressIndicator 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.graphicsLayer 9 | import androidx.compose.ui.unit.dp 10 | import androidx.compose.ui.unit.Dp 11 | import androidx.compose.foundation.layout.requiredHeight 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import dev.toastbits.composekit.components.utils.composable.RecomposeOnInterval 14 | import com.toasterofbread.spmp.service.playercontroller.PlayerState 15 | import com.toasterofbread.spmp.ui.layout.nowplaying.* 16 | 17 | @Composable 18 | internal fun MinimisedProgressBar( 19 | height: Dp, 20 | modifier: Modifier = Modifier 21 | ) { 22 | val player: PlayerState = LocalPlayerState.current 23 | val expansion: PlayerExpansionState = LocalNowPlayingExpansion.current 24 | 25 | RecomposeOnInterval(POSITION_UPDATE_INTERVAL_MS) { state -> 26 | state 27 | 28 | LinearProgressIndicator( 29 | progress = player.status.getProgress(), 30 | color = player.getNPOnBackground(), 31 | trackColor = player.getNPOnBackground().copy(alpha = 0.5f), 32 | modifier = modifier 33 | .requiredHeight(height) 34 | .fillMaxWidth() 35 | .graphicsLayer { 36 | alpha = 1f - expansion.get() 37 | } 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/container/VideoBackground.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.nowplaying.container 2 | 3 | import androidx.compose.runtime.* 4 | import androidx.compose.ui.Modifier 5 | import com.toasterofbread.spmp.platform.doesPlatformSupportVideoPlayback 6 | import com.toasterofbread.spmp.platform.SongVideoPlayback 7 | import com.toasterofbread.spmp.service.playercontroller.PlayerState 8 | import com.toasterofbread.spmp.ui.layout.nowplaying.PlayerExpansionState 9 | import com.toasterofbread.spmp.model.mediaitem.song.Song 10 | import LocalPlayerState 11 | import LocalNowPlayingExpansion 12 | 13 | @Composable 14 | internal fun VideoBackground( 15 | modifier: Modifier = Modifier, 16 | getAlpha: () -> Float = { 1f } 17 | ): Boolean { 18 | if (!doesPlatformSupportVideoPlayback()) { 19 | return false 20 | } 21 | 22 | val player: PlayerState = LocalPlayerState.current 23 | val expansion: PlayerExpansionState = LocalNowPlayingExpansion.current 24 | val current_song: Song? by player.status.song_state 25 | 26 | current_song?.id?.also { song_id -> 27 | return SongVideoPlayback( 28 | song_id, 29 | modifier = modifier, 30 | getPositionMs = { player.status.getPositionMs() }, 31 | fill = true, 32 | getAlpha = { 33 | getAlpha() * expansion.get().coerceIn(0f..1f) 34 | } 35 | ) 36 | } 37 | 38 | return false 39 | } 40 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/overlay/NotifImageOverlayMenu.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.nowplaying.overlay 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | expect fun notifImagePlayerOverlayMenuButtonText(): String? 7 | 8 | expect class NotifImagePlayerOverlayMenu(): PlayerOverlayMenu 9 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/NowPlayingQueuePage.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.nowplaying.queue 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.unit.Dp 7 | import com.toasterofbread.spmp.platform.FormFactor 8 | import com.toasterofbread.spmp.service.playercontroller.PlayerState 9 | import com.toasterofbread.spmp.ui.layout.nowplaying.NowPlayingPage 10 | import com.toasterofbread.spmp.ui.layout.nowplaying.NowPlayingTopBar 11 | 12 | class NowPlayingQueuePage: NowPlayingPage() { 13 | override fun shouldShow(player: PlayerState, form_factor: FormFactor): Boolean = 14 | form_factor == FormFactor.PORTRAIT 15 | 16 | @Composable 17 | override fun Page(page_height: Dp, top_bar: NowPlayingTopBar, content_padding: PaddingValues, swipe_modifier: Modifier, modifier: Modifier) { 18 | QueueTab(page_height, modifier, top_bar = top_bar, padding_modifier = swipe_modifier, content_padding = content_padding) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/radiobuilder/RadioBuilderIcon.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.radiobuilder 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.offset 5 | import androidx.compose.foundation.layout.requiredSize 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.Add 9 | import androidx.compose.material.icons.filled.Radio 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.unit.dp 15 | 16 | @Composable 17 | fun RadioBuilderIcon(modifier: Modifier = Modifier) { 18 | Box(modifier.requiredSize(RADIO_BUILDER_ICON_WIDTH_DP.dp, 24.dp)) { 19 | Icon(Icons.Default.Radio, null, Modifier.align(Alignment.Center)) 20 | 21 | val add_icon_size = 15.dp 22 | Icon( 23 | Icons.Default.Add, 24 | null, 25 | Modifier 26 | .align(Alignment.TopCenter) 27 | .size(add_icon_size) 28 | .offset( 29 | x = (-5).dp + ((RADIO_BUILDER_ICON_WIDTH_DP.dp) / 2), 30 | y = (-5).dp 31 | ) 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/youtubemusiclogin/YoutubeMusicWebviewLogin.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.youtubemusiclogin 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import dev.toastbits.ytmkt.impl.youtubei.YoutubeiApi 6 | import io.ktor.http.Headers 7 | 8 | @Composable 9 | internal expect fun YoutubeMusicWebviewLogin( 10 | api: YoutubeiApi, 11 | login_url: String, 12 | modifier: Modifier = Modifier, 13 | onFinished: (Result?) -> Unit 14 | ) 15 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/theme/ApplicationTheme.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") 2 | package com.toasterofbread.spmp.ui.theme 3 | 4 | import androidx.compose.animation.core.AnimationSpec 5 | import androidx.compose.animation.core.animateFloatAsState 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.hoverable 8 | import androidx.compose.foundation.interaction.MutableInteractionSource 9 | import androidx.compose.foundation.interaction.collectIsHoveredAsState 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.composed 14 | import androidx.compose.ui.draw.scale 15 | import androidx.compose.ui.input.pointer.PointerIcon 16 | import androidx.compose.ui.input.pointer.pointerHoverIcon 17 | import dev.toastbits.composekit.util.thenIf 18 | 19 | fun Modifier.appHover( 20 | button: Boolean = false, 21 | expand: Boolean = false, 22 | hover_scale: Float = if (button) 0.95f else 0.97f, 23 | animation_spec: AnimationSpec = tween(100) 24 | ): Modifier = composed { 25 | val interaction_source: MutableInteractionSource = remember { MutableInteractionSource() } 26 | val hovered: Boolean by interaction_source.collectIsHoveredAsState() 27 | 28 | val actual_hover_scale: Float = if (expand) 2f - hover_scale else hover_scale 29 | val scale: Float by animateFloatAsState( 30 | if (hovered) actual_hover_scale else 1f, 31 | animationSpec = animation_spec 32 | ) 33 | 34 | return@composed this 35 | .hoverable(interaction_source) 36 | .scale(scale) 37 | .thenIf(button) { 38 | pointerHoverIcon(PointerIcon.Hand) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/util/ListUtils.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.util 2 | 3 | fun MutableList.removeLastBuiltIn(): T { 4 | if (isEmpty()) { 5 | throw NoSuchElementException() 6 | } 7 | 8 | return removeAt(size - 1) 9 | } 10 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/util/SongLikedStatusToggleTarget.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.util 2 | 3 | import dev.toastbits.ytmkt.model.external.SongLikedStatus 4 | 5 | fun SongLikedStatus?.getToggleTarget(): SongLikedStatus = 6 | when (this) { 7 | null, 8 | SongLikedStatus.NEUTRAL -> SongLikedStatus.LIKED 9 | SongLikedStatus.DISLIKED, 10 | SongLikedStatus.LIKED -> SongLikedStatus.NEUTRAL 11 | } 12 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/widget/action/LyricsWidgetClickAction.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.action 2 | 3 | import kotlinx.serialization.Serializable 4 | import org.jetbrains.compose.resources.StringResource 5 | import spmp.shared.generated.resources.Res 6 | import spmp.shared.generated.resources.widget_click_action_lyrics_hide_until_next_song 7 | import spmp.shared.generated.resources.widget_click_action_lyrics_toggle_furigana 8 | 9 | @Serializable 10 | enum class LyricsWidgetClickAction(val nameResource: StringResource): TypeWidgetClickAction { 11 | TOGGLE_FURIGANA(Res.string.widget_click_action_lyrics_toggle_furigana), 12 | HIDE_UNTIL_NEXT_SONG(Res.string.widget_click_action_lyrics_hide_until_next_song); 13 | } 14 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/widget/action/SongImageWidgetClickAction.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.action 2 | 3 | import kotlinx.serialization.Serializable 4 | import org.jetbrains.compose.resources.StringResource 5 | 6 | @Serializable 7 | enum class SongImageWidgetClickAction(val nameResource: StringResource): TypeWidgetClickAction 8 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/widget/action/SongQueueWidgetClickAction.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.action 2 | 3 | import kotlinx.serialization.Serializable 4 | import org.jetbrains.compose.resources.StringResource 5 | 6 | @Serializable 7 | enum class SongQueueWidgetClickAction(val nameResource: StringResource): TypeWidgetClickAction 8 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/widget/action/SplitImageControlsWidgetClickAction.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.action 2 | 3 | import kotlinx.serialization.Serializable 4 | import org.jetbrains.compose.resources.StringResource 5 | 6 | @Serializable 7 | enum class SplitImageControlsWidgetClickAction(val nameResource: StringResource): TypeWidgetClickAction 8 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/widget/action/TypeWidgetClickAction.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.action 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | sealed interface TypeWidgetClickAction 7 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/widget/configuration/enum/WidgetSectionTheme.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.configuration.enum 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.Color 5 | import dev.toastbits.composekit.theme.core.ui.LocalComposeKitTheme 6 | import dev.toastbits.composekit.theme.core.vibrantAccent 7 | import dev.toastbits.composekit.util.blendWith 8 | import dev.toastbits.composekit.util.thenIf 9 | import kotlinx.serialization.Serializable 10 | 11 | @Serializable 12 | data class WidgetSectionTheme( 13 | val mode: Mode, 14 | val opacity: Float = DEFAULT_OPACITY 15 | ) { 16 | enum class Mode(val opacity_configurable: Boolean = true) { 17 | BACKGROUND, ACCENT, TRANSPARENT(opacity_configurable = false) 18 | } 19 | 20 | companion object { 21 | const val DEFAULT_OPACITY: Float = 1f 22 | } 23 | } 24 | 25 | val WidgetSectionTheme.colour: Color 26 | @Composable 27 | get() = mode.colour.thenIf(mode.opacity_configurable) { copy(alpha = opacity) } 28 | 29 | val WidgetSectionTheme.Mode.colour: Color 30 | @Composable 31 | get() = with (LocalComposeKitTheme.current) { 32 | when (this@colour) { 33 | WidgetSectionTheme.Mode.BACKGROUND -> background 34 | WidgetSectionTheme.Mode.ACCENT -> card.blendWith(accent, 0.2f) 35 | WidgetSectionTheme.Mode.TRANSPARENT -> Color.Transparent 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/widget/configuration/enum/WidgetStyledBorderMode.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.configuration.enum 2 | 3 | enum class WidgetStyledBorderMode { 4 | WAVE, NONE 5 | } 6 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/widget/configuration/type/LyricsWidgetConfigDefaultsMask.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.configuration.type 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | internal data class LyricsWidgetConfigDefaultsMask( 7 | val furigana_mode: Boolean = true, 8 | override val click_action: Boolean = true 9 | ): TypeConfigurationDefaultsMask { 10 | override fun applyTo( 11 | config: LyricsWidgetConfig, 12 | default: LyricsWidgetConfig 13 | ): LyricsWidgetConfig = 14 | LyricsWidgetConfig( 15 | furigana_mode = if (furigana_mode) default.furigana_mode else config.furigana_mode, 16 | click_action = if (click_action) default.click_action else config.click_action 17 | ) 18 | 19 | override fun setClickAction(click_action: Boolean): TypeConfigurationDefaultsMask = 20 | copy(click_action = click_action) 21 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/widget/configuration/type/SongImageWidgetConfigDefaultsMask.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.configuration.type 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class SongImageWidgetConfigDefaultsMask( 7 | override val click_action: Boolean = true 8 | ): TypeConfigurationDefaultsMask { 9 | override fun applyTo( 10 | config: SongImageWidgetConfig, 11 | default: SongImageWidgetConfig 12 | ): SongImageWidgetConfig = 13 | SongImageWidgetConfig( 14 | click_action = if (click_action) default.click_action else config.click_action 15 | ) 16 | 17 | override fun setClickAction(click_action: Boolean): TypeConfigurationDefaultsMask = 18 | copy(click_action = click_action) 19 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/widget/configuration/type/SongQueueWidgetConfigDefaultsMask.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.configuration.type 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class SongQueueWidgetConfigDefaultsMask( 7 | val show_current_song: Boolean = true, 8 | val next_songs_to_show: Boolean = true, 9 | override val click_action: Boolean = true 10 | ): TypeConfigurationDefaultsMask { 11 | override fun applyTo( 12 | config: SongQueueWidgetConfig, 13 | default: SongQueueWidgetConfig 14 | ): SongQueueWidgetConfig = 15 | SongQueueWidgetConfig( 16 | show_current_song = if (show_current_song) default.show_current_song else config.show_current_song, 17 | next_songs_to_show = if (next_songs_to_show) default.next_songs_to_show else config.next_songs_to_show, 18 | click_action = if (click_action) default.click_action else config.click_action 19 | ) 20 | 21 | override fun setClickAction(click_action: Boolean): TypeConfigurationDefaultsMask = 22 | copy(click_action = click_action) 23 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/widget/configuration/type/TypeConfigurationDefaultsMask.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.widget.configuration.type 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | sealed interface TypeConfigurationDefaultsMask> { 7 | val click_action: Boolean 8 | fun applyTo(config: C, default: C): C 9 | 10 | fun setClickAction(click_action: Boolean): TypeConfigurationDefaultsMask 11 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/youtubeapi/YtmApiType.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.youtubeapi 2 | 3 | import com.toasterofbread.spmp.platform.AppContext 4 | import com.toasterofbread.spmp.platform.getDataLanguage 5 | import com.toasterofbread.spmp.resources.Language 6 | import dev.toastbits.ytmkt.impl.unimplemented.UnimplementedYtmApi 7 | import dev.toastbits.ytmkt.impl.youtubei.YoutubeiApi 8 | import dev.toastbits.ytmkt.model.YtmApi 9 | 10 | enum class YtmApiType { 11 | YOUTUBE_MUSIC, 12 | UNIMPLEMENTED_FOR_TESTING; 13 | 14 | fun isSelectable(): Boolean = this != UNIMPLEMENTED_FOR_TESTING 15 | 16 | fun getDefaultUrl(): String = 17 | when (this) { 18 | YOUTUBE_MUSIC -> YoutubeiApi.DEFAULT_API_URL 19 | UNIMPLEMENTED_FOR_TESTING -> "" 20 | } 21 | 22 | fun instantiate(context: AppContext, api_url: String, data_language: Language): YtmApi = 23 | when (this) { 24 | YOUTUBE_MUSIC -> SpMpYoutubeiApi(context, api_url, data_language) 25 | UNIMPLEMENTED_FOR_TESTING -> UnimplementedYtmApi() 26 | } 27 | 28 | companion object { 29 | val DEFAULT: YtmApiType = YOUTUBE_MUSIC 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/youtubeapi/lyrics/Furigana.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.youtubeapi.lyrics 2 | 3 | import com.toasterofbread.spmp.model.lyrics.SongLyrics 4 | 5 | fun interface LyricsFuriganaTokeniser { 6 | fun mergeAndFuriganiseTerms(terms: List, romanise: Boolean): List 7 | 8 | companion object { 9 | private var instance: LyricsFuriganaTokeniser? = null 10 | 11 | suspend fun getInstance(): LyricsFuriganaTokeniser? { 12 | if (instance == null) { 13 | instance = createFuriganaTokeniserImpl() 14 | } 15 | return instance 16 | } 17 | 18 | } 19 | } 20 | 21 | internal expect suspend fun createFuriganaTokeniserImpl(): LyricsFuriganaTokeniser? 22 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/youtubeapi/lyrics/Kugou.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.youtubeapi.lyrics 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.Color 5 | import com.toasterofbread.spmp.model.lyrics.SongLyrics 6 | import com.toasterofbread.spmp.platform.AppContext 7 | import com.toasterofbread.spmp.platform.getUiLanguage 8 | import com.toasterofbread.spmp.youtubeapi.lyrics.kugou.loadKugouLyrics 9 | import com.toasterofbread.spmp.youtubeapi.lyrics.kugou.searchKugouLyrics 10 | import org.jetbrains.compose.resources.stringResource 11 | import spmp.shared.generated.resources.Res 12 | import spmp.shared.generated.resources.lyrics_source_kugou 13 | import kotlin.time.Duration 14 | 15 | internal class KugouLyricsSource(source_idx: Int): LyricsSource(source_idx) { 16 | @Composable 17 | override fun getReadable(): String = stringResource(Res.string.lyrics_source_kugou) 18 | override fun getColour(): Color = Color(0xFF50A6FB) 19 | override fun getUrlOfId(id: String): String? = null 20 | 21 | override suspend fun getLyrics( 22 | lyrics_id: String, 23 | context: AppContext 24 | ): Result = runCatching { 25 | val lines: List> = loadKugouLyrics(lyrics_id, context.getUiLanguage().toTag()).getOrThrow() 26 | 27 | return@runCatching SongLyrics( 28 | LyricsReference(source_index, lyrics_id), 29 | SongLyrics.SyncType.LINE_SYNC, 30 | lines 31 | ) 32 | } 33 | 34 | override suspend fun searchForLyrics( 35 | title: String, 36 | artist_name: String?, 37 | album_name: String?, 38 | duration: Duration? 39 | ): Result> { 40 | return searchKugouLyrics(title, artist_name) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/youtubeapi/lyrics/Lrclib.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.youtubeapi.lyrics 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.Color 5 | import com.toasterofbread.spmp.platform.AppContext 6 | import com.toasterofbread.spmp.model.lyrics.SongLyrics 7 | import com.toasterofbread.spmp.youtubeapi.lyrics.lrclib.loadLrclibLyrics 8 | import com.toasterofbread.spmp.youtubeapi.lyrics.lrclib.searchLrclibLyrics 9 | import kotlin.time.Duration 10 | 11 | internal class LrclibLyricsSource(source_idx: Int): LyricsSource(source_idx) { 12 | @Composable 13 | override fun getReadable(): String = "lrclib" 14 | override fun getColour(): Color = Color(0x0C0E41) 15 | override fun getUrlOfId(id: String): String? = null 16 | 17 | override fun supportsLyricsBySong(): Boolean = false 18 | override fun supportsLyricsBySearching(): Boolean = true 19 | 20 | override suspend fun getLyrics( 21 | lyrics_id: String, 22 | context: AppContext 23 | ): Result = runCatching { 24 | val lines: List> = loadLrclibLyrics(lyrics_id).getOrThrow() 25 | 26 | return@runCatching SongLyrics( 27 | LyricsReference(source_index, lyrics_id), 28 | SongLyrics.SyncType.LINE_SYNC, 29 | lines 30 | ) 31 | } 32 | 33 | override suspend fun searchForLyrics( 34 | title: String, 35 | artist_name: String?, 36 | album: String?, 37 | duration: Duration? 38 | ): Result> = runCatching { 39 | return searchLrclibLyrics(title, artist_name, album, duration) 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/toasterofbread/spmp/youtubeapi/lyrics/petit/ParsePetitLyrics.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.youtubeapi.lyrics.petit 2 | 3 | import com.toasterofbread.spmp.model.lyrics.SongLyrics 4 | import nl.adaptivity.xmlutil.serialization.XML 5 | import kotlinx.serialization.Serializable 6 | import nl.adaptivity.xmlutil.serialization.XmlElement 7 | import nl.adaptivity.xmlutil.serialization.XmlSerialName 8 | 9 | internal fun parseTimedLyrics(raw_data: String): Result>> { 10 | val data: wsy = XML.decodeFromString(wsy.serializer(), raw_data) 11 | 12 | return Result.success( 13 | data.lines.mapIndexed { line_index, line -> 14 | line.words.map { word -> 15 | SongLyrics.Term( 16 | listOf(SongLyrics.Term.Text(word.wordstring)), 17 | line_index, 18 | word.starttime, 19 | word.endtime 20 | ) 21 | } 22 | } 23 | ) 24 | } 25 | 26 | @Serializable 27 | internal data class wsy( 28 | @XmlElement 29 | val linenum: Int, 30 | @XmlSerialName("line", "", "") 31 | val lines: List 32 | ) { 33 | @Serializable 34 | data class Line( 35 | @XmlElement 36 | val linestring: String, 37 | @XmlElement 38 | val wordnum: Int, 39 | @XmlSerialName("word", "", "") 40 | val words: List 41 | ) 42 | @Serializable 43 | data class Word( 44 | @XmlElement 45 | val starttime: Long, 46 | @XmlElement 47 | val endtime: Long, 48 | @XmlElement 49 | val wordstring: String, 50 | @XmlElement 51 | val chanum: Int 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/toasterofbread/spmp/db/Version.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE Version ( 2 | version INTEGER NOT NULL PRIMARY KEY 3 | ); 4 | 5 | INSERT INTO Version (version) VALUES (0); 6 | -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/toasterofbread/spmp/db/mediaitem/Artist.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE Artist ( 2 | id TEXT NOT NULL PRIMARY KEY, 3 | 4 | subscribe_channel_id TEXT, 5 | shuffle_playlist_id TEXT, 6 | subscriber_count INTEGER, 7 | 8 | -- 9 | 10 | subscribed INTEGER, 11 | 12 | FOREIGN KEY (id) REFERENCES MediaItem(id) 13 | ); 14 | 15 | byHidden: SELECT Artist.id FROM Artist, MediaItem WHERE MediaItem.id == Artist.id AND MediaItem.hidden == :hidden; 16 | byId: SELECT id FROM Artist WHERE id == :id; 17 | 18 | insertById { 19 | INSERT OR IGNORE INTO MediaItem(id) VALUES(:id); 20 | INSERT OR IGNORE INTO Artist(id) VALUES(:id); 21 | } 22 | removeById { 23 | DELETE FROM Artist WHERE id == :id; 24 | DELETE FROM MediaItem WHERE id == :id; 25 | } 26 | 27 | subscribeChannelIdById: SELECT subscribe_channel_id FROM Artist WHERE id == :id; 28 | updateSubscribeChannelIdById: UPDATE Artist SET subscribe_channel_id = :subscribe_channel_id WHERE id == :id; 29 | 30 | shufflePlaylistIdById: SELECT shuffle_playlist_id FROM Artist WHERE id == :id; 31 | updateShufflePlaylistIdById: UPDATE Artist SET shuffle_playlist_id = :shuffle_playlist_id WHERE id == :id; 32 | 33 | subscriberCountById: SELECT subscriber_count FROM Artist WHERE id == :id; 34 | updateSubscriberCountById: UPDATE Artist SET subscriber_count = :subscriber_count WHERE id == :id; 35 | 36 | subscribedById: SELECT subscribed FROM Artist WHERE id == :id; 37 | updateSubscribedById: UPDATE Artist SET subscribed = :subscribed WHERE id == :id; 38 | -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/toasterofbread/spmp/db/mediaitem/ArtistLayoutItem.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE ArtistLayoutItem ( 2 | item_index INTEGER NOT NULL, 3 | item_id TEXT NOT NULL, 4 | item_type INTEGER NOT NULL, 5 | artist_id TEXT NOT NULL, 6 | layout_index INTEGER NOT NULL, 7 | 8 | PRIMARY KEY (item_index, artist_id, layout_index), 9 | FOREIGN KEY (artist_id, layout_index) REFERENCES ArtistLayout(artist_id, layout_index), 10 | FOREIGN KEY (item_id) REFERENCES MediaItem(id) 11 | ); 12 | 13 | byLayoutIndex: 14 | SELECT item_index, item_id, item_type 15 | FROM ArtistLayoutItem 16 | WHERE artist_id == :artist_id AND layout_index == :layout_index 17 | ORDER BY item_index; 18 | 19 | insertItemAtIndex: 20 | INSERT INTO ArtistLayoutItem(artist_id, layout_index, item_id, item_type, item_index) 21 | VALUES(:artist_id, :layout_index, :item_id, :item_type, :item_index); 22 | 23 | removeItemAtIndex: 24 | DELETE FROM ArtistLayoutItem 25 | WHERE artist_id == :artist_id AND layout_index == :layout_index AND item_index == :item_index; 26 | 27 | updateItemIndex: 28 | UPDATE ArtistLayoutItem SET item_index = :to WHERE item_index == :from AND artist_id == :artist_id AND layout_index == :layout_index; 29 | 30 | itemCount: 31 | SELECT COUNT(*) FROM ArtistLayoutItem 32 | WHERE artist_id = :artist_id AND layout_index = :layout_index; 33 | 34 | clearItems: 35 | DELETE FROM ArtistLayoutItem WHERE artist_id = :artist_id AND layout_index = :layout_index AND item_index >= :from_index; 36 | -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/toasterofbread/spmp/db/mediaitem/MediaItem.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE MediaItem ( 2 | id TEXT NOT NULL PRIMARY KEY, 3 | 4 | loaded INTEGER, 5 | 6 | title TEXT, 7 | custom_title TEXT, 8 | description TEXT, 9 | 10 | thumb_url_a TEXT, 11 | thumb_url_b TEXT, 12 | 13 | theme_colour INTEGER, 14 | hidden INTEGER 15 | ); 16 | 17 | loadedById: SELECT loaded FROM MediaItem WHERE id == :id; 18 | updateLoadedById: UPDATE MediaItem SET loaded = :loaded WHERE id == :id; 19 | 20 | titleById: SELECT title FROM MediaItem WHERE id == :id; 21 | updateTitleById: UPDATE MediaItem SET title = :title WHERE id == :id; 22 | 23 | customTitleById: SELECT custom_title FROM MediaItem WHERE id == :id; 24 | updateCustomTitleById: UPDATE MediaItem SET custom_title = :custom_title WHERE id == :id; 25 | 26 | descriptionById: SELECT description FROM MediaItem WHERE id == :id; 27 | updateDescriptionById: UPDATE MediaItem SET description = :description WHERE id == :id; 28 | 29 | thumbnailProviderById: SELECT thumb_url_a, thumb_url_b FROM MediaItem WHERE id == :id; 30 | updateThumbnailProviderById: UPDATE MediaItem SET thumb_url_a = :url_a, thumb_url_b = :url_b WHERE id == :id; 31 | 32 | themeColourById: SELECT theme_colour FROM MediaItem WHERE id == :id; 33 | updateThemeColourById: UPDATE MediaItem SET theme_colour = :theme_colour WHERE id == :id; 34 | 35 | isHiddenById: SELECT hidden FROM MediaItem WHERE id == :id; 36 | updateIsHiddenById: UPDATE MediaItem SET hidden = :hidden WHERE id == :id; 37 | 38 | -- 39 | 40 | activeTitleById: SELECT IFNULL(custom_title, title) FROM MediaItem WHERE id == :id; 41 | 42 | loaded: SELECT id, loaded FROM MediaItem; 43 | -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/toasterofbread/spmp/db/mediaitem/MediaItemPlayCount.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE MediaItemPlayCount( 2 | day INTEGER NOT NULL, 3 | item_id TEXT NOT NULL, 4 | 5 | play_count INTEGER NOT NULL DEFAULT 0, 6 | 7 | FOREIGN KEY (item_id) REFERENCES MediaItem(id), 8 | PRIMARY KEY (day, item_id) 9 | ); 10 | 11 | byDay: SELECT item_id, play_count FROM MediaItemPlayCount WHERE day == :day; 12 | byItemId: SELECT day, play_count FROM MediaItemPlayCount WHERE item_id == :item_id; 13 | byItemIdSince: SELECT day, play_count FROM MediaItemPlayCount WHERE item_id == :item_id AND day >= :since_day; 14 | 15 | insertOrIgnore: 16 | INSERT OR IGNORE INTO MediaItemPlayCount(day, item_id) VALUES (:day, :item_id); 17 | 18 | increment: 19 | UPDATE MediaItemPlayCount SET play_count = play_count + :by WHERE item_id == :item_id AND day == :day; 20 | 21 | updateItemId: 22 | UPDATE MediaItemPlayCount SET item_id = :to_id WHERE item_id = :from_id; 23 | -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/toasterofbread/spmp/db/mediaitem/PinnedItem.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE PinnedItem ( 2 | id TEXT NOT NULL, 3 | type INTEGER NOT NULL, 4 | 5 | -- Local playlists do not have DB entries 6 | -- FOREIGN KEY (id) REFERENCES MediaItem(id), 7 | 8 | PRIMARY KEY (id, type) 9 | ); 10 | 11 | insert: INSERT OR IGNORE INTO PinnedItem(id, type) VALUES (:id, :type); 12 | remove: DELETE FROM PinnedItem WHERE id == :id AND type == :type; 13 | 14 | getAll: SELECT * FROM PinnedItem; 15 | count: SELECT count(*) FROM PinnedItem; 16 | countByItem: SELECT count(*) FROM PinnedItem WHERE id == :id AND type == :type; 17 | -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/toasterofbread/spmp/db/mediaitem/PlaylistItem.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE PlaylistItem ( 2 | item_index INTEGER NOT NULL, 3 | playlist_id TEXT NOT NULL, 4 | song_id TEXT NOT NULL, 5 | 6 | PRIMARY KEY (item_index, playlist_id), 7 | FOREIGN KEY (playlist_id) REFERENCES Playlist(id), 8 | FOREIGN KEY (song_id) REFERENCES Song(id) 9 | ); 10 | 11 | byPlaylistId: 12 | SELECT * 13 | FROM PlaylistItem 14 | WHERE playlist_id == :playlist_id 15 | ORDER BY item_index; 16 | 17 | insertItemAtIndex: 18 | INSERT OR REPLACE INTO PlaylistItem(playlist_id, song_id, item_index) 19 | VALUES(:playlist_id, :song_id, :item_index); 20 | 21 | removeItemAtIndex: 22 | DELETE FROM PlaylistItem 23 | WHERE playlist_id == :playlist_id AND item_index == :item_index; 24 | 25 | removeByPlaylistId: 26 | DELETE FROM PlaylistItem 27 | WHERE playlist_id == :playlist_id; 28 | 29 | updateItemIndex: 30 | UPDATE PlaylistItem SET item_index = :to WHERE item_index == :from AND playlist_id == :playlist_id; 31 | 32 | itemCount: 33 | SELECT COUNT(CASE WHEN PlaylistItem.playlist_id == :playlist_id THEN 1 ELSE NULL END) FROM PlaylistItem; 34 | 35 | clearItems: 36 | DELETE FROM PlaylistItem WHERE playlist_id = :playlist_id AND item_index >= :from_index; 37 | -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/toasterofbread/spmp/db/persistentqueue/PersistentQueueItem.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE PersistentQueueItem ( 2 | item_index INTEGER NOT NULL PRIMARY KEY, 3 | id TEXT NOT NULL, 4 | 5 | FOREIGN KEY (id) REFERENCES MediaItem(id) 6 | ); 7 | 8 | clear: 9 | DELETE FROM PersistentQueueItem; 10 | 11 | insert: 12 | INSERT INTO PersistentQueueItem(item_index, id) VALUES(:item_index, :id); 13 | 14 | get: 15 | SELECT * FROM PersistentQueueItem ORDER BY item_index ASC; 16 | -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/toasterofbread/spmp/db/persistentqueue/PersistentQueueMetadata.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE PersistentQueueMetadata ( 2 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 3 | queue_index INTEGER NOT NULL, 4 | playback_position_ms INTEGER NOT NULL 5 | ); 6 | 7 | set { 8 | DELETE FROM PersistentQueueMetadata; 9 | INSERT INTO PersistentQueueMetadata(id, queue_index, playback_position_ms) VALUES(:id, :queue_index, :playback_position_ms); 10 | } 11 | 12 | get: 13 | SELECT * FROM PersistentQueueMetadata; 14 | 15 | clear: 16 | DELETE FROM PersistentQueueMetadata; 17 | -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/toasterofbread/spmp/db/songfeed/SongFeedFilter.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE SongFeedFilter ( 2 | filter_index INTEGER NOT NULL PRIMARY KEY, 3 | 4 | params TEXT NOT NULL, 5 | text_data TEXT NOT NULL 6 | ); 7 | 8 | getAll: SELECT * FROM SongFeedFilter ORDER BY filter_index; 9 | 10 | insert: INSERT INTO SongFeedFilter(filter_index, params, text_data) VALUES(:filter_index, :params, :text_data); 11 | -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/toasterofbread/spmp/db/songfeed/SongFeedRow.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE SongFeedRow ( 2 | row_index INTEGER NOT NULL PRIMARY KEY, 3 | 4 | -- items SongFeedRowItem 5 | creation_time INTEGER NOT NULL, 6 | continuation_token TEXT, 7 | layout_type INTEGER, 8 | 9 | title_data TEXT, 10 | view_more_type INTEGER, 11 | view_more_data TEXT 12 | ); 13 | 14 | getAll: SELECT * FROM SongFeedRow ORDER BY row_index; 15 | 16 | insert: 17 | INSERT INTO SongFeedRow(row_index, creation_time, continuation_token, layout_type, title_data, view_more_type, view_more_data) 18 | VALUES (:row_index, :creation_time, :continuation_token, :layout_type, :title_data, :view_more_type, :view_more_data); 19 | 20 | clearAllFeedData { 21 | DELETE FROM SongFeedRowItem; 22 | DELETE FROM SongFeedRow; 23 | DELETE FROM SongFeedFilter; 24 | } 25 | -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/toasterofbread/spmp/db/songfeed/SongFeedRowItem.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE SongFeedRowItem ( 2 | row_index INTEGER NOT NULL, 3 | item_index INTEGER NOT NULL, 4 | 5 | item_id TEXT NOT NULL, 6 | item_type INTEGER NOT NULL, 7 | 8 | FOREIGN KEY (row_index) REFERENCES SongFeedRow(row_index), 9 | FOREIGN KEY (item_id) REFERENCES MediaItem(id), 10 | 11 | PRIMARY KEY (row_index, item_index) 12 | ); 13 | 14 | byRowIndex: SELECT * FROM SongFeedRowItem WHERE row_index == :row_index ORDER BY item_index; 15 | 16 | insert: INSERT INTO SongFeedRowItem(row_index, item_index, item_id, item_type) VALUES(:row_index, :item_index, :item_id, :item_type); 17 | -------------------------------------------------------------------------------- /shared/src/commonMain/sqldelight/com/toasterofbread/spmp/db/widget/AndroidWidget.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE AndroidWidget ( 2 | id INTEGER NOT NULL PRIMARY KEY, 3 | configuration TEXT NOT NULL 4 | ); 5 | 6 | configurationById: SELECT configuration FROM AndroidWidget WHERE id == :id; 7 | 8 | insertOrReplace: INSERT OR REPLACE INTO AndroidWidget(id, configuration) VALUES(:id, :configuration); 9 | 10 | remove: DELETE FROM AndroidWidget WHERE id == :id; 11 | -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/SpMp.desktop.kt: -------------------------------------------------------------------------------- 1 | import org.jetbrains.skiko.hostOs 2 | import org.jetbrains.skiko.OS 3 | 4 | actual fun isWindowTransparencySupported(): Boolean = 5 | when (hostOs) { 6 | OS.Linux -> true 7 | else -> false 8 | } 9 | -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/com/toasterofbread/spmp/model/appaction/shortcut/DefaultShortcuts.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.appaction.shortcut 2 | 3 | import androidx.compose.ui.input.pointer.PointerButton 4 | import androidx.compose.ui.input.key.Key 5 | import com.toasterofbread.spmp.ui.component.shortcut.trigger.* 6 | import com.toasterofbread.spmp.model.appaction.* 7 | 8 | actual fun getPlatformDefaultShortcuts(): List = 9 | listOf( 10 | Shortcut( 11 | MouseButtonShortcutTrigger(PointerButton.Back.index), 12 | OtherAppAction(OtherAppAction.Action.NAVIGATE_BACK) 13 | ), 14 | Shortcut( 15 | MouseButtonShortcutTrigger(5), 16 | OtherAppAction(OtherAppAction.Action.NAVIGATE_BACK) 17 | ), 18 | Shortcut( 19 | KeyboardShortcutTrigger(Key.F11.keyCode), 20 | OtherAppAction(OtherAppAction.Action.TOGGLE_FULLSCREEN) 21 | ) 22 | ) 23 | -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/SqlDriver.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import app.cash.sqldelight.db.SqlDriver 4 | import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver 5 | import com.toasterofbread.spmp.db.Database 6 | import java.io.File 7 | 8 | actual fun AppContext.getSqlDriver(): SqlDriver { 9 | val database_file: File = getFilesDir()!!.resolve("spmp_database.db").file 10 | database_file.parentFile.mkdirs() 11 | 12 | val database_exists: Boolean = database_file.exists() 13 | val driver: SqlDriver = JdbcSqliteDriver("jdbc:sqlite:" + database_file.absolutePath) 14 | 15 | if (!database_exists) { 16 | Database.Schema.create(driver) 17 | } 18 | 19 | return driver 20 | } 21 | -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/VideoPlayback.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import com.toasterofbread.spmp.platform.ffmpeg.VideoPlayerFFmpeg 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.runtime.Composable 6 | 7 | actual fun doesPlatformSupportVideoPlayback(): Boolean = true 8 | 9 | @Composable 10 | actual fun VideoPlayback( 11 | url: String, 12 | getPositionMs: () -> Long, 13 | modifier: Modifier, 14 | fill: Boolean, 15 | getAlpha: () -> Float 16 | ): Boolean { 17 | return VideoPlayerFFmpeg( 18 | url = url, 19 | getPositionMs = getPositionMs, 20 | modifier = modifier, 21 | fill = fill, 22 | getAlpha = getAlpha 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/download/LocalSongMetadataProcessor.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.download 2 | 3 | actual val LocalSongMetadataProcessor: MetadataProcessor = JAudioTaggerMetadataProcessor 4 | -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/playerservice/SpMs.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice 2 | 3 | import org.jetbrains.skiko.OS 4 | import org.jetbrains.skiko.hostOs 5 | import java.io.File 6 | import java.lang.System.getenv 7 | import com.toasterofbread.spmp.platform.AppContext 8 | import dev.toastbits.composekit.context.PlatformFile 9 | import dev.toastbits.composekit.context.fromFile 10 | 11 | actual fun getSpMsMachineId(context: AppContext): String { 12 | val id_file: File = 13 | when (hostOs) { 14 | OS.Linux -> File("/tmp/") 15 | OS.Windows -> File("${getenv("USERPROFILE")!!}/AppData/Local/") 16 | else -> throw NotImplementedError(hostOs.name) 17 | }.resolve("spmp_machine_id.txt") 18 | 19 | return getSpMsMachineIdFromFile(PlatformFile.fromFile(id_file, context)) 20 | } 21 | -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/visualiser/MusicVisualiser.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.visualiser 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.Modifier 6 | 7 | actual class MusicVisualiser { 8 | @Composable 9 | actual fun Visualiser(colour: Color, modifier: Modifier, opacity: Float) {} 10 | 11 | actual companion object { 12 | actual fun isSupported(): Boolean = false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/overlay/NotifImageOverlayMenu.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.nowplaying.overlay 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.ImageBitmap 5 | import com.toasterofbread.spmp.model.mediaitem.song.Song 6 | 7 | @Composable 8 | actual fun notifImagePlayerOverlayMenuButtonText(): String? = null 9 | 10 | actual class NotifImagePlayerOverlayMenu actual constructor(): PlayerOverlayMenu() { 11 | @Composable 12 | override fun Menu( 13 | getSong: () -> Song?, 14 | getExpansion: () -> Float, 15 | openMenu: (PlayerOverlayMenu?) -> Unit, 16 | getSeekState: () -> Any, 17 | getCurrentSongThumb: () -> ImageBitmap?, 18 | ) { 19 | throw IllegalAccessError() 20 | } 21 | 22 | override fun closeOnTap(): Boolean { 23 | throw IllegalAccessError() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /shared/src/jvmMain/kotlin/Coroutines.jvm.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.Dispatchers 2 | import kotlinx.coroutines.CoroutineDispatcher 3 | 4 | actual val Dispatchers.PlatformIO: CoroutineDispatcher 5 | get() = Dispatchers.IO 6 | -------------------------------------------------------------------------------- /shared/src/jvmMain/kotlin/com/toasterofbread/spmp/model/settings/category/StreamingSettings.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.settings.category 2 | 3 | import dev.toastbits.ytmkt.formats.NewPipeVideoFormatsEndpoint 4 | import dev.toastbits.ytmkt.formats.PipedVideoFormatsEndpoint 5 | import dev.toastbits.ytmkt.formats.VideoFormatsEndpoint 6 | import dev.toastbits.ytmkt.formats.YoutubeiVideoFormatsEndpoint 7 | import dev.toastbits.ytmkt.model.YtmApi 8 | 9 | actual fun VideoFormatsEndpointType.isAvailable(): Boolean = true 10 | 11 | actual fun VideoFormatsEndpointType.instantiate(api: YtmApi): VideoFormatsEndpoint = 12 | when (this) { 13 | VideoFormatsEndpointType.YOUTUBEI -> YoutubeiVideoFormatsEndpoint(api) 14 | VideoFormatsEndpointType.PIPED -> PipedVideoFormatsEndpoint(api) 15 | VideoFormatsEndpointType.NEWPIPE -> NewPipeVideoFormatsEndpoint(api) 16 | } 17 | -------------------------------------------------------------------------------- /shared/src/jvmMain/kotlin/com/toasterofbread/spmp/platform/download/LocalSongMetadataProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.download 2 | 3 | import com.toasterofbread.spmp.model.mediaitem.song.Song 4 | import com.toasterofbread.spmp.model.mediaitem.song.SongData 5 | import dev.toastbits.composekit.context.PlatformFile 6 | import com.toasterofbread.spmp.platform.AppContext 7 | 8 | expect val LocalSongMetadataProcessor: MetadataProcessor 9 | 10 | interface MetadataProcessor { 11 | suspend fun addMetadataToLocalSong(song: Song, file: PlatformFile, file_extension: String, context: AppContext) 12 | suspend fun readLocalSongMetadata(file: PlatformFile, context: AppContext, match_id: String? = null, load_data: Boolean = true): SongData? 13 | } 14 | -------------------------------------------------------------------------------- /shared/src/main/res/values-ldrtl/bools.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | -------------------------------------------------------------------------------- /shared/src/main/res/values/bools.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | -------------------------------------------------------------------------------- /shared/src/notAndroidMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.notAndroid.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import com.toasterofbread.spmp.model.mediaitem.song.Song 4 | import dev.toastbits.spms.socketapi.shared.SpMsPlayerRepeatMode 5 | import dev.toastbits.spms.socketapi.shared.SpMsPlayerState 6 | 7 | actual abstract class PlayerListener actual constructor() { 8 | actual open fun onSongTransition(song: Song?, manual: Boolean) {} 9 | actual open fun onStateChanged(state: SpMsPlayerState) {} 10 | actual open fun onPlayingChanged(is_playing: Boolean) {} 11 | actual open fun onRepeatModeChanged(repeat_mode: SpMsPlayerRepeatMode) {} 12 | actual open fun onVolumeChanged(volume: Float) {} 13 | actual open fun onDurationChanged(duration_ms: Long) {} 14 | actual open fun onSeeked(position_ms: Long) {} 15 | actual open fun onUndoStateChanged() {} 16 | actual open fun onSongAdded(index: Int, song: Song) {} 17 | actual open fun onSongRemoved(index: Int, song: Song) {} 18 | actual open fun onSongMoved(from: Int, to: Int) {} 19 | actual open fun onEvents() {} 20 | } 21 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/Coroutines.wasmJs.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.Dispatchers 2 | import kotlinx.coroutines.CoroutineDispatcher 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlin.js.Promise 5 | 6 | actual val Dispatchers.PlatformIO: CoroutineDispatcher 7 | get() = Dispatchers.Main 8 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/PlatformTheme.wasmJs.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.Composable 2 | import dev.toastbits.composekit.theme.core.ThemeValues 3 | 4 | @Composable 5 | internal actual fun PlatformTheme(theme: ThemeValues, content: @Composable () -> Unit) { 6 | content() 7 | } 8 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/SpMp.wasmJs.kt: -------------------------------------------------------------------------------- 1 | actual fun isWindowTransparencySupported(): Boolean = false 2 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/model/appaction/shortcut/DefaultShortcuts.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.appaction.shortcut 2 | 3 | internal actual fun getPlatformDefaultShortcuts(): List = 4 | listOf() 5 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/model/mediaitem/library/LocalSongSyncLoader.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.mediaitem.library 2 | 3 | import com.toasterofbread.spmp.platform.AppContext 4 | import com.toasterofbread.spmp.platform.download.DownloadStatus 5 | 6 | internal actual class LocalSongSyncLoader actual constructor(): SyncLoader() { 7 | override suspend fun internalPerformSync(context: AppContext): Map = emptyMap() 8 | } 9 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/model/settings/category/StreamingSettings.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.model.settings.category 2 | 3 | import dev.toastbits.ytmkt.formats.PipedVideoFormatsEndpoint 4 | import dev.toastbits.ytmkt.formats.VideoFormatsEndpoint 5 | import dev.toastbits.ytmkt.formats.YoutubeiVideoFormatsEndpoint 6 | import dev.toastbits.ytmkt.model.YtmApi 7 | 8 | actual fun VideoFormatsEndpointType.isAvailable(): Boolean = 9 | when (this) { 10 | VideoFormatsEndpointType.YOUTUBEI -> true 11 | VideoFormatsEndpointType.PIPED -> true 12 | VideoFormatsEndpointType.NEWPIPE -> false 13 | } 14 | 15 | actual fun VideoFormatsEndpointType.instantiate(api: YtmApi): VideoFormatsEndpoint = 16 | when (this) { 17 | VideoFormatsEndpointType.YOUTUBEI -> YoutubeiVideoFormatsEndpoint(api) 18 | VideoFormatsEndpointType.PIPED -> PipedVideoFormatsEndpoint(api) 19 | VideoFormatsEndpointType.NEWPIPE -> throw IllegalStateException() 20 | } 21 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/platform/DiscordStatus.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import com.toasterofbread.spmp.model.mediaitem.MediaItem 4 | import dev.toastbits.ytmkt.model.external.ThumbnailProvider 5 | 6 | // TODO 7 | actual class DiscordStatus actual constructor( 8 | context: AppContext, 9 | application_id: String, 10 | account_token: String? 11 | ) { 12 | actual companion object { 13 | actual fun isSupported(): Boolean = false 14 | actual fun isAccountTokenRequired(): Boolean = false 15 | actual fun getWarningText(): String? = null 16 | } 17 | 18 | actual enum class Status { ONLINE, IDLE, DO_NOT_DISTURB } 19 | actual enum class Type { PLAYING, STREAMING, LISTENING, WATCHING, COMPETING } 20 | 21 | actual fun close() { 22 | throw IllegalStateException() 23 | } 24 | 25 | actual suspend fun shouldUpdateStatus(): Boolean { 26 | throw IllegalStateException() 27 | } 28 | 29 | actual fun setActivity( 30 | name: String, 31 | type: Type, 32 | status: Status, 33 | state: String?, 34 | details: String?, 35 | timestamps: Pair?, 36 | large_image: String?, 37 | small_image: String?, 38 | large_text: String?, 39 | small_text: String?, 40 | buttons: List>? 41 | ) { 42 | throw IllegalStateException() 43 | } 44 | 45 | actual suspend fun getCustomImages( 46 | image_items: List, 47 | target_quality: ThumbnailProvider.Quality 48 | ): Result> { 49 | throw IllegalStateException() 50 | } 51 | } 52 | 53 | actual suspend fun getDiscordAccountInfo(account_token: String?): Result = 54 | Result.failure(IllegalStateException()) 55 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/platform/ImageBitmap.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.ImageBitmap 5 | import androidx.compose.ui.graphics.asComposeImageBitmap 6 | import androidx.compose.ui.graphics.asSkiaBitmap 7 | import androidx.compose.ui.graphics.toComposeImageBitmap 8 | import org.jetbrains.skia.Bitmap 9 | import org.jetbrains.skia.IRect 10 | import org.jetbrains.skia.Image 11 | 12 | actual fun createImageBitmapUtil(): ImageBitmapUtil? = null 13 | 14 | actual fun ByteArray.toImageBitmap(): ImageBitmap = 15 | Bitmap.makeFromImage(Image.makeFromEncoded(this)).asComposeImageBitmap() 16 | 17 | actual fun ImageBitmap.toByteArray(): ByteArray = 18 | asSkiaBitmap().readPixels()!! 19 | 20 | actual fun ImageBitmap.crop(x: Int, y: Int, width: Int, height: Int): ImageBitmap { 21 | val bitmap = Bitmap() 22 | asSkiaBitmap().extractSubset(bitmap, IRect.makeXYWH(x, y, width, height)) 23 | return bitmap.asComposeImageBitmap() 24 | } 25 | 26 | actual fun ImageBitmap.getPixel(x: Int, y: Int): Color = 27 | Color(asSkiaBitmap().getColor(x, y)) 28 | 29 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/platform/SqlDriver.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import app.cash.sqldelight.db.SqlDriver 4 | import app.cash.sqldelight.driver.worker.createDefaultWebWorkerDriver 5 | 6 | actual fun AppContext.getSqlDriver(): SqlDriver = 7 | createDefaultWebWorkerDriver() 8 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/platform/VideoPlayback.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | 6 | actual fun isVideoPlaybackSupported(): Boolean = false 7 | 8 | @Composable 9 | actual fun VideoPlayback( 10 | url: String, 11 | getPositionMs: () -> Long, 12 | modifier: Modifier, 13 | fill: Boolean, 14 | getAlpha: () -> Float 15 | ): Boolean { 16 | throw IllegalStateException() 17 | } 18 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/platform/WebViewLogin.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | 6 | actual fun isWebViewLoginSupported(): Boolean = false 7 | 8 | internal actual suspend fun initWebViewLogin( 9 | context: AppContext, 10 | onProgress: (Float, String?) -> Unit 11 | ): Result = Result.failure(IllegalStateException()) 12 | 13 | @Composable 14 | actual fun WebViewLogin( 15 | initial_url: String, 16 | onClosed: () -> Unit, 17 | shouldShowPage: (url: String) -> Boolean, 18 | modifier: Modifier, 19 | loading_message: String?, 20 | base_cookies: String, 21 | user_agent: String?, 22 | onRequestIntercepted: suspend (WebViewRequest, openUrl: (String) -> Unit, getCookies: suspend (String) -> List>) -> Unit 23 | ) { 24 | throw IllegalStateException() 25 | } 26 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/platform/download/PlayerDownloadManager.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.download 2 | 3 | import com.toasterofbread.spmp.model.mediaitem.song.Song 4 | import com.toasterofbread.spmp.platform.AppContext 5 | import com.toasterofbread.spmp.service.playercontroller.DownloadRequestCallback 6 | 7 | actual class PlayerDownloadManager actual constructor(context: AppContext) { 8 | actual open class DownloadStatusListener actual constructor() { 9 | actual open fun onDownloadAdded(status: DownloadStatus) {} 10 | actual open fun onDownloadRemoved(id: String) {} 11 | actual open fun onDownloadChanged(status: DownloadStatus) {} 12 | } 13 | 14 | actual fun addDownloadStatusListener(listener: DownloadStatusListener) {} 15 | 16 | actual fun removeDownloadStatusListener(listener: DownloadStatusListener) {} 17 | 18 | actual suspend fun getDownload(song: Song): DownloadStatus? = null 19 | 20 | actual suspend fun getDownloads(): List = emptyList() 21 | 22 | actual fun canStartDownload(): Boolean = false 23 | 24 | actual fun startDownload( 25 | song: Song, 26 | silent: Boolean, 27 | custom_uri: String?, 28 | download_lyrics: Boolean, 29 | direct: Boolean, 30 | callback: DownloadRequestCallback? 31 | ) { 32 | throw IllegalStateException() 33 | } 34 | 35 | actual suspend fun deleteSongLocalAudioFile(song: Song) {} 36 | 37 | actual fun release() {} 38 | } 39 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/platform/playerservice/LocalServer.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.toasterofbread.spmp.platform.AppContext 5 | import kotlinx.coroutines.Job 6 | 7 | actual object LocalServer { 8 | actual fun isAvailable(): Boolean = false 9 | 10 | @Composable 11 | actual fun getLocalServerUnavailabilityReason(): String? = null 12 | 13 | actual fun startLocalServer( 14 | context: AppContext, 15 | port: Int 16 | ): Job { 17 | throw IllegalStateException() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/platform/playerservice/SpMs.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.playerservice 2 | 3 | import com.toasterofbread.spmp.platform.AppContext 4 | 5 | actual fun getSpMsMachineId(context: AppContext): String = 6 | generateNewSpMsMachineId() 7 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/platform/visualiser/MusicVisualiser.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.platform.visualiser 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.graphics.Color 6 | 7 | actual class MusicVisualiser { 8 | @Composable 9 | actual fun Visualiser( 10 | colour: Color, 11 | modifier: Modifier, 12 | opacity: Float 13 | ) { 14 | throw IllegalStateException() 15 | } 16 | 17 | actual companion object { 18 | actual fun isSupported(): Boolean = false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/overlay/NotifImageOverlayMenu.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.nowplaying.overlay 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.ImageBitmap 5 | import com.toasterofbread.spmp.model.mediaitem.song.Song 6 | 7 | @Composable 8 | actual fun notifImagePlayerOverlayMenuButtonText(): String? = null 9 | 10 | actual class NotifImagePlayerOverlayMenu actual constructor(): PlayerOverlayMenu() { 11 | @Composable 12 | override fun Menu( 13 | getSong: () -> Song?, 14 | getExpansion: () -> Float, 15 | openMenu: (PlayerOverlayMenu?) -> Unit, 16 | getSeekState: () -> Any, 17 | getCurrentSongThumb: () -> ImageBitmap?, 18 | ) { 19 | throw IllegalStateException() 20 | } 21 | 22 | override fun closeOnTap(): Boolean { 23 | throw IllegalStateException() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/ui/layout/youtubemusiclogin/YoutubeMusicWebviewLogin.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.ui.layout.youtubemusiclogin 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import dev.toastbits.ytmkt.impl.youtubei.YoutubeiApi 6 | import io.ktor.http.Headers 7 | 8 | @Composable 9 | internal actual fun YoutubeMusicWebviewLogin( 10 | api: YoutubeiApi, 11 | login_url: String, 12 | modifier: Modifier, 13 | onFinished: (Result?) -> Unit 14 | ) { 15 | throw IllegalStateException() 16 | } 17 | -------------------------------------------------------------------------------- /shared/src/wasmJsMain/kotlin/com/toasterofbread/spmp/youtubeapi/lyrics/Furigana.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.toasterofbread.spmp.youtubeapi.lyrics 2 | 3 | // TODO 4 | internal actual suspend fun createFuriganaTokeniserImpl(): LyricsFuriganaTokeniser? = null 5 | --------------------------------------------------------------------------------