├── .env.example
├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ ├── release-docker.yml
│ ├── release-linux.yml
│ ├── release-mac.yml
│ ├── release-win.yml
│ ├── test-castlabs.yml
│ └── test-notarytool.yml
├── .gitignore
├── .vscode
└── settings.json
├── Dockerfile
├── LICENSE
├── README.md
├── app
├── .gitignore
├── .vscode
│ └── settings.json
├── assets
│ ├── entitlements.mac.plist
│ └── icons
│ │ ├── ampcast-grayscale.png
│ │ ├── ampcast.ico
│ │ ├── ampcast.png
│ │ ├── ampcast.svg
│ │ ├── ampcast128x128.png
│ │ ├── ampcast16x16.png
│ │ ├── ampcast256x256.png
│ │ ├── ampcast32x32.png
│ │ ├── ampcast64x64.png
│ │ ├── setup.gif
│ │ ├── setup.ico
│ │ └── setup.png
├── package-lock.json
├── package.json
├── prettier.config.js
├── scripts
│ ├── afterPack.js
│ ├── afterSign.js
│ └── uninstaller.nsh
├── src
│ ├── icon.png
│ ├── main.js
│ ├── menu.js
│ ├── preload.js
│ ├── server.js
│ ├── splash.html
│ ├── splash.png
│ └── store.js
└── www
│ ├── 404.html
│ ├── apple-touch-icon.png
│ ├── auth
│ ├── lastfm
│ │ └── callback
│ │ │ └── index.html
│ ├── plex
│ │ └── callback
│ │ │ └── index.html
│ ├── spotify
│ │ └── callback
│ │ │ └── index.html
│ └── tidal
│ │ └── callback
│ │ └── index.html
│ ├── favicon.ico
│ ├── favicon.svg
│ ├── icon-192.png
│ ├── icon-512.png
│ ├── manifest.json
│ ├── preview.jpg
│ └── privacy-policy.html
├── docker-compose.yml
├── libs
├── icecast-metadata-player-1.17.12.main.min.js
├── icecast-metadata-player-1.17.12.mediasource.min.js
└── icecast-metadata-player-1.17.12.synaudio.min.js
├── package-lock.json
├── package.json
├── postcss.config.cjs
├── prettier.config.js
├── proxy-login.js
├── server.js
├── src
├── assets
│ ├── pixel.png.base64.ts
│ └── silence.mp3.base64.ts
├── components
│ ├── Actions
│ │ ├── Actions.tsx
│ │ ├── AddToPlaylistDialog.scss
│ │ ├── AddToPlaylistDialog.tsx
│ │ ├── CreatePlaylistDialog.scss
│ │ ├── CreatePlaylistDialog.tsx
│ │ ├── PlaylistActions.tsx
│ │ ├── index.ts
│ │ ├── performAction.ts
│ │ ├── useEditablePlaylistsPager.ts
│ │ └── usePlaylistItems.ts
│ ├── App
│ │ ├── App.tsx
│ │ ├── AppContent.tsx
│ │ ├── AppDragRegion.scss
│ │ ├── AppDragRegion.tsx
│ │ ├── AppTitle.scss
│ │ ├── AppTitle.tsx
│ │ ├── DesktopWarning.scss
│ │ ├── DesktopWarning.tsx
│ │ ├── PortUnavailable.scss
│ │ ├── PortUnavailable.tsx
│ │ ├── index.ts
│ │ ├── useAppSettings.ts
│ │ ├── useBrowser.ts
│ │ ├── useConnectivity.ts
│ │ ├── useGlobalActions.ts
│ │ ├── useMediaSession.ts
│ │ ├── usePreload.ts
│ │ ├── usePreventDrop.ts
│ │ └── usePseudoClasses.ts
│ ├── Badges
│ │ ├── Badge.scss
│ │ ├── Badge.tsx
│ │ ├── Badges.scss
│ │ ├── Badges.tsx
│ │ └── index.ts
│ ├── Button
│ │ ├── CopyButton.scss
│ │ ├── CopyButton.tsx
│ │ ├── IconButton.scss
│ │ ├── IconButton.tsx
│ │ ├── IconButtons.scss
│ │ ├── IconButtons.tsx
│ │ └── index.ts
│ ├── CoverArt
│ │ ├── CoverArt.scss
│ │ ├── CoverArt.tsx
│ │ └── index.ts
│ ├── CoverArtVisualizer
│ │ ├── CoverArtVisualizer.tsx
│ │ └── index.ts
│ ├── DatePicker
│ │ ├── DatePicker.tsx
│ │ ├── MonthPicker.tsx
│ │ ├── YearPicker.tsx
│ │ └── index.ts
│ ├── Dialog
│ │ ├── Dialog.scss
│ │ ├── Dialog.tsx
│ │ ├── DialogButtons.tsx
│ │ ├── alert.scss
│ │ ├── alert.tsx
│ │ ├── confirm.scss
│ │ ├── confirm.tsx
│ │ ├── error.tsx
│ │ ├── index.ts
│ │ ├── prompt.scss
│ │ ├── prompt.tsx
│ │ └── showDialog.tsx
│ ├── Errors
│ │ ├── BSOD.scss
│ │ ├── BSOD.tsx
│ │ ├── ErrorBox.scss
│ │ ├── ErrorBox.tsx
│ │ ├── ErrorReport.scss
│ │ ├── ErrorReport.tsx
│ │ ├── HandledError.tsx
│ │ └── UnhandledError.tsx
│ ├── ExternalLink
│ │ ├── ExternalLink.scss
│ │ ├── ExternalLink.tsx
│ │ └── index.ts
│ ├── Icon
│ │ ├── Flag.tsx
│ │ ├── Icon.scss
│ │ ├── Icon.tsx
│ │ ├── SvgDefs.scss
│ │ ├── SvgDefs.tsx
│ │ └── index.ts
│ ├── ListView
│ │ ├── ColumnResizer.tsx
│ │ ├── DetailsBox.scss
│ │ ├── DetailsBox.tsx
│ │ ├── ListBox.scss
│ │ ├── ListBox.tsx
│ │ ├── ListView.scss
│ │ ├── ListView.tsx
│ │ ├── ListViewBody.tsx
│ │ ├── ListViewBodyCell.tsx
│ │ ├── ListViewBodyRow.tsx
│ │ ├── ListViewHead.tsx
│ │ ├── ListViewHeadCell.tsx
│ │ ├── index.ts
│ │ ├── useColumns.ts
│ │ └── useSelectedItems.ts
│ ├── Login
│ │ ├── ConnectionLogging.scss
│ │ ├── ConnectionLogging.tsx
│ │ ├── CredentialsButton.tsx
│ │ ├── CredentialsRequired.scss
│ │ ├── CredentialsRequired.tsx
│ │ ├── DefaultLogin.tsx
│ │ ├── HTTPDownloadLink.tsx
│ │ ├── Login.scss
│ │ ├── Login.tsx
│ │ ├── LoginButton.tsx
│ │ ├── LoginDialog.scss
│ │ ├── LoginDialog.tsx
│ │ ├── LoginRequired.tsx
│ │ ├── RestrictedAccessWarning.scss
│ │ ├── RestrictedAccessWarning.tsx
│ │ ├── ServiceLink.tsx
│ │ └── index.ts
│ ├── Media
│ │ ├── Interstitial.scss
│ │ ├── Interstitial.tsx
│ │ ├── Media.scss
│ │ ├── Media.tsx
│ │ ├── MediaButtons.tsx
│ │ ├── PlaybackState.tsx
│ │ ├── ProgressBar.scss
│ │ ├── ProgressBar.tsx
│ │ ├── Static.scss
│ │ ├── Static.tsx
│ │ ├── Visualizer.scss
│ │ ├── VisualizerControls.scss
│ │ ├── VisualizerControls.tsx
│ │ ├── index.ts
│ │ ├── useCanLockVisualizer.ts
│ │ ├── useInterstitialState.ts
│ │ ├── useLoadingState.ts
│ │ └── useVideoSourceIcon.tsx
│ ├── MediaBrowser
│ │ ├── Albums.tsx
│ │ ├── Artists.tsx
│ │ ├── DefaultBrowser.tsx
│ │ ├── ErrorScreen.tsx
│ │ ├── FilterBrowser.tsx
│ │ ├── FilterSelect.scss
│ │ ├── FilterSelect.tsx
│ │ ├── FolderBrowser.tsx
│ │ ├── HistoryBrowser.tsx
│ │ ├── MediaBrowser.scss
│ │ ├── MediaBrowser.tsx
│ │ ├── MediaItems.tsx
│ │ ├── MediaSourceSelector.tsx
│ │ ├── PageHeader.scss
│ │ ├── PageHeader.tsx
│ │ ├── PagedItems.tsx
│ │ ├── PinnedPlaylist.scss
│ │ ├── PinnedPlaylist.tsx
│ │ ├── Playlists.tsx
│ │ ├── RecentlyPlayedBrowser.tsx
│ │ ├── index.ts
│ │ ├── showMediaSourceOptions.tsx
│ │ ├── useAlbumTracksLayout.ts
│ │ ├── useErrorScreen.tsx
│ │ ├── useHistoryPager.ts
│ │ ├── useNoInternetError.tsx
│ │ └── useRecentlyPlayedPager.ts
│ ├── MediaControls
│ │ ├── MediaButton.scss
│ │ ├── MediaButton.tsx
│ │ ├── MediaControls.scss
│ │ ├── MediaControls.tsx
│ │ ├── PlaylistMenu.tsx
│ │ ├── VolumeControl.scss
│ │ ├── VolumeControl.tsx
│ │ ├── index.ts
│ │ └── usePlaylistMenu.tsx
│ ├── MediaInfo
│ │ ├── CurrentlyPlaying.tsx
│ │ ├── CurrentlyPlayingDialog.tsx
│ │ ├── CurrentlyPlayingTabs.tsx
│ │ ├── MediaDetails.scss
│ │ ├── MediaDetails.tsx
│ │ ├── MediaInfo.scss
│ │ ├── MediaInfo.tsx
│ │ ├── MediaInfoDialog.scss
│ │ ├── MediaInfoDialog.tsx
│ │ ├── MediaInfoTabs.scss
│ │ ├── MediaInfoTabs.tsx
│ │ ├── VisualizerInfo.scss
│ │ ├── VisualizerInfo.tsx
│ │ ├── useActiveItem.ts
│ │ └── useMediaInfoDialog.ts
│ ├── MediaLibrary
│ │ ├── MediaLibrary.scss
│ │ ├── MediaLibrary.tsx
│ │ └── index.ts
│ ├── MediaList
│ │ ├── AlbumList.tsx
│ │ ├── ArtistList.tsx
│ │ ├── FolderItemList.tsx
│ │ ├── MediaItemList.tsx
│ │ ├── MediaList.scss
│ │ ├── MediaList.tsx
│ │ ├── MediaListStatusBar.scss
│ │ ├── MediaListStatusBar.tsx
│ │ ├── PlaylistList.tsx
│ │ ├── ProgressRing.scss
│ │ ├── ProgressRing.tsx
│ │ ├── index.ts
│ │ ├── showActionsMenu.tsx
│ │ ├── useIsPlaylistPlayable.ts
│ │ ├── useMediaListLayout.tsx
│ │ ├── useOnDragStart.ts
│ │ └── useViewClassName.ts
│ ├── MediaPlayback
│ │ ├── MediaPlayback.scss
│ │ ├── MediaPlayback.tsx
│ │ └── index.ts
│ ├── MediaSources
│ │ ├── MediaServiceLabel.tsx
│ │ ├── MediaSourceLabel.scss
│ │ ├── MediaSourceLabel.tsx
│ │ ├── MediaSources.tsx
│ │ ├── ProvidedBy.tsx
│ │ ├── index.ts
│ │ └── useMediaSources.tsx
│ ├── MiniPlayer
│ │ ├── MiniPlayer.scss
│ │ ├── MiniPlayer.tsx
│ │ └── index.ts
│ ├── Playlist
│ │ ├── Playlist.scss
│ │ ├── Playlist.tsx
│ │ ├── index.ts
│ │ ├── showActionsMenu.tsx
│ │ ├── usePlaylistInject.tsx
│ │ └── usePlaylistLayout.tsx
│ ├── PopupMenu
│ │ ├── PopupMenu.scss
│ │ ├── PopupMenu.tsx
│ │ ├── PopupMenuItem.tsx
│ │ ├── PopupMenuItemCheckbox.tsx
│ │ ├── PopupMenuSeparator.tsx
│ │ ├── index.ts
│ │ └── showPopupMenu.tsx
│ ├── Scrollable
│ │ ├── FixedHeader.tsx
│ │ ├── Scrollable.scss
│ │ ├── Scrollable.tsx
│ │ ├── Scrollbar.scss
│ │ ├── Scrollbar.tsx
│ │ ├── index.ts
│ │ ├── scrollbarReducer.ts
│ │ └── useScrollbarState.ts
│ ├── SearchBar
│ │ ├── SearchBar.scss
│ │ ├── SearchBar.tsx
│ │ └── index.ts
│ ├── Settings
│ │ ├── AdvancedSettings
│ │ │ ├── AdvancedSettings.scss
│ │ │ ├── AdvancedSettings.tsx
│ │ │ ├── Backup.scss
│ │ │ ├── Backup.tsx
│ │ │ ├── Logs.scss
│ │ │ ├── Logs.tsx
│ │ │ ├── Troubleshooting.tsx
│ │ │ ├── index.ts
│ │ │ ├── useBackupEntries.ts
│ │ │ └── useLogs.ts
│ │ ├── AppSettings
│ │ │ ├── AppPreferences.tsx
│ │ │ ├── AppSettings.scss
│ │ │ ├── AppSettings.tsx
│ │ │ ├── AppSettingsGeneral.tsx
│ │ │ └── index.ts
│ │ ├── AppearanceSettings
│ │ │ ├── AppearanceSettings.tsx
│ │ │ ├── AppearanceSettingsGeneral.scss
│ │ │ ├── AppearanceSettingsGeneral.tsx
│ │ │ ├── ThemeEditor
│ │ │ │ ├── SaveThemeDialog.scss
│ │ │ │ ├── SaveThemeDialog.tsx
│ │ │ │ ├── ThemeColor.tsx
│ │ │ │ ├── ThemeColorPair.tsx
│ │ │ │ ├── ThemeEditor.scss
│ │ │ │ ├── ThemeEditor.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── saveTheme.tsx
│ │ │ │ └── useSuggestedColors.ts
│ │ │ ├── UserThemes.scss
│ │ │ ├── UserThemes.tsx
│ │ │ ├── confirmDeleteTheme.ts
│ │ │ ├── confirmOverwriteTheme.ts
│ │ │ ├── importThemeFromFile.ts
│ │ │ ├── index.ts
│ │ │ ├── useCurrentTheme.ts
│ │ │ ├── useDefaultThemes.ts
│ │ │ └── useUserThemes.ts
│ │ ├── MediaLibrarySettings
│ │ │ ├── AudioSettings.scss
│ │ │ ├── AudioSettings.tsx
│ │ │ ├── CredentialsDialog.scss
│ │ │ ├── CredentialsDialog.tsx
│ │ │ ├── CredentialsInput.tsx
│ │ │ ├── CredentialsRegistration.tsx
│ │ │ ├── DisconnectButton.scss
│ │ │ ├── DisconnectButton.tsx
│ │ │ ├── LookupSettings.tsx
│ │ │ ├── MediaServiceCredentials.scss
│ │ │ ├── MediaServiceCredentials.tsx
│ │ │ ├── MediaServiceList.scss
│ │ │ ├── MediaServiceList.tsx
│ │ │ ├── MediaServiceSettings.tsx
│ │ │ ├── MediaServiceSettingsGeneral.scss
│ │ │ ├── MediaServiceSettingsGeneral.tsx
│ │ │ ├── MediaServicesSettings.tsx
│ │ │ ├── MediaServicesSettingsGeneral.tsx
│ │ │ ├── PersonalMediaServerInfo.scss
│ │ │ ├── PersonalMediaServerInfo.tsx
│ │ │ ├── PersonalMediaServerSettings.scss
│ │ │ ├── PersonalMediaServerSettings.tsx
│ │ │ ├── PinnedSettings.scss
│ │ │ ├── PinnedSettings.tsx
│ │ │ ├── ScrobblingSettings.tsx
│ │ │ ├── confirmDisconnectServices.scss
│ │ │ ├── confirmDisconnectServices.tsx
│ │ │ ├── useAudioLibraries.ts
│ │ │ ├── usePinsForService.ts
│ │ │ └── useServerInfo.ts
│ │ ├── SettingsDialog.scss
│ │ ├── SettingsDialog.tsx
│ │ ├── VisualizerSettings
│ │ │ ├── AmbientVideoSettings.tsx
│ │ │ ├── AmpshaderSettings.tsx
│ │ │ ├── ButterchurnSettings.scss
│ │ │ ├── ButterchurnSettings.tsx
│ │ │ ├── CoverArtSettings.tsx
│ │ │ ├── VisualizerFavorites.scss
│ │ │ ├── VisualizerFavorites.tsx
│ │ │ ├── VisualizerRandomness.scss
│ │ │ ├── VisualizerRandomness.tsx
│ │ │ ├── VisualizerSettings.scss
│ │ │ ├── VisualizerSettings.tsx
│ │ │ ├── VisualizerSettingsDialog.tsx
│ │ │ ├── VisualizerSettingsGeneral.tsx
│ │ │ ├── index.ts
│ │ │ ├── useVisualizerFavorites.ts
│ │ │ └── useVisualizerRandomness.ts
│ │ ├── index.ts
│ │ └── useSettingsSources.tsx
│ ├── Splitter
│ │ ├── Splitter.scss
│ │ ├── Splitter.tsx
│ │ ├── index.ts
│ │ ├── layout.scss
│ │ └── layoutSettings.ts
│ ├── StarRating
│ │ ├── StarRating.scss
│ │ ├── StarRating.tsx
│ │ └── index.ts
│ ├── StartupWizard
│ │ ├── StartupWizard.scss
│ │ ├── StartupWizard.tsx
│ │ └── index.ts
│ ├── StatusBar
│ │ ├── StatusBar.scss
│ │ ├── StatusBar.tsx
│ │ └── index.ts
│ ├── SunClock
│ │ ├── SunClock.scss
│ │ ├── SunClock.tsx
│ │ └── index.ts
│ ├── TabList
│ │ ├── Tab.tsx
│ │ ├── TabList.scss
│ │ ├── TabList.tsx
│ │ ├── TabPanel.tsx
│ │ └── index.ts
│ ├── TextBox
│ │ ├── TextBox.scss
│ │ ├── TextBox.tsx
│ │ └── index.ts
│ ├── Time
│ │ ├── Time.tsx
│ │ └── index.ts
│ └── TreeView
│ │ ├── TreeView.scss
│ │ ├── TreeView.tsx
│ │ ├── TreeViewNode.tsx
│ │ ├── index.ts
│ │ ├── useNodeIds.ts
│ │ └── useTreeViewState.ts
├── hooks
│ ├── useBaseFontSize.ts
│ ├── useCurrentTime.ts
│ ├── useCurrentTrack.ts
│ ├── useCurrentVisualizer.ts
│ ├── useCurrentlyPlaying.ts
│ ├── useDebouncedValue.ts
│ ├── useDuration.ts
│ ├── useFactoryReset.ts
│ ├── useFirstValue.ts
│ ├── useFontSize.ts
│ ├── useIsLoggedIn.ts
│ ├── useIsOnLine.ts
│ ├── useIsPlaying.ts
│ ├── useKeyboardBusy.ts
│ ├── useMediaServices.ts
│ ├── useMiniPlayerActive.ts
│ ├── useMouseBusy.ts
│ ├── useObservable.ts
│ ├── useOnResize.ts
│ ├── usePager.tsx
│ ├── usePaused.ts
│ ├── usePlaybackState.ts
│ ├── usePreferences.ts
│ ├── usePrevious.ts
│ ├── useSearch.ts
│ ├── useSorting.ts
│ ├── useSource.ts
│ ├── useSubject.ts
│ ├── useThrottledValue.ts
│ ├── useVisualizerProviders.ts
│ ├── useVisualizerSettings.ts
│ └── useYouTubeVideoInfo.ts
├── html
│ └── index.html
├── index.tsx
├── registerServiceWorker.tsx
├── service-worker.js
├── services
│ ├── actions
│ │ └── actionsStore.ts
│ ├── ampcastElectron.ts
│ ├── apple
│ │ ├── AppleBitrate.ts
│ │ ├── MusicKit.d.ts
│ │ ├── MusicKitPager.ts
│ │ ├── apple.ts
│ │ ├── appleAuth.ts
│ │ ├── appleSettings.ts
│ │ ├── bootstrap.ts
│ │ ├── components
│ │ │ ├── AppleCredentials.tsx
│ │ │ ├── AppleLogin.tsx
│ │ │ ├── AppleStreamingSettings.tsx
│ │ │ ├── useCredentials.ts
│ │ │ └── useMusicKit.ts
│ │ ├── index.ts
│ │ ├── musicKitPlayer.ts
│ │ └── musicKitUtils.ts
│ ├── audio
│ │ ├── OmniAnalyserNode.ts
│ │ ├── OmniAudioContext.ts
│ │ ├── audio.ts
│ │ ├── audioSettings.ts
│ │ └── index.ts
│ ├── buildConfig.ts
│ ├── constants.ts
│ ├── emby
│ │ ├── EmbyPager.ts
│ │ ├── components
│ │ │ └── EmbyLoginDialog.tsx
│ │ ├── emby.ts
│ │ ├── embyApi.ts
│ │ ├── embyAuth.ts
│ │ ├── embyReporting.ts
│ │ ├── embyScrobbler.ts
│ │ ├── embySettings.ts
│ │ └── index.ts
│ ├── errors.ts
│ ├── globalDrag.ts
│ ├── i18n.ts
│ ├── jellyfin
│ │ ├── JellyfinPager.ts
│ │ ├── index.ts
│ │ ├── jellyfin.ts
│ │ ├── jellyfinApi.ts
│ │ ├── jellyfinAuth.ts
│ │ └── jellyfinSettings.ts
│ ├── lastfm
│ │ ├── LastFmHistoryPager.ts
│ │ ├── LastFmPager.ts
│ │ ├── components
│ │ │ ├── LastFmCredentials.tsx
│ │ │ ├── LastFmHistoryBrowser.tsx
│ │ │ ├── LastFmLogin.tsx
│ │ │ ├── LastFmScrobblesBrowser.tsx
│ │ │ ├── useCredentials.ts
│ │ │ └── useHistoryStart.ts
│ │ ├── index.ts
│ │ ├── lastfm.ts
│ │ ├── lastfmApi.ts
│ │ ├── lastfmAuth.ts
│ │ ├── lastfmScrobbler.ts
│ │ ├── lastfmSettings.ts
│ │ └── types.d.ts
│ ├── listenbrainz
│ │ ├── ListenBrainzHistoryPager.ts
│ │ ├── ListenBrainzLikesPager.ts
│ │ ├── ListenBrainzNewAlbumsPager.ts
│ │ ├── ListenBrainzPlaylistItemsPager.ts
│ │ ├── ListenBrainzPlaylistsPager.ts
│ │ ├── ListenBrainzStatsPager.ts
│ │ ├── components
│ │ │ ├── ListenBrainzHistoryBrowser.tsx
│ │ │ ├── ListenBrainzLoginDialog.scss
│ │ │ ├── ListenBrainzLoginDialog.tsx
│ │ │ ├── ListenBrainzScrobblesBrowser.tsx
│ │ │ └── useHistoryStart.ts
│ │ ├── index.ts
│ │ ├── listenbrainz.ts
│ │ ├── listenbrainzApi.ts
│ │ ├── listenbrainzAuth.ts
│ │ ├── listenbrainzScrobbler.ts
│ │ ├── listenbrainzSettings.ts
│ │ └── types.d.ts
│ ├── localdb
│ │ └── listens.ts
│ ├── lookup
│ │ ├── index.ts
│ │ ├── lookup.ts
│ │ ├── lookupEvents.ts
│ │ └── lookupSettings.ts
│ ├── mediaPlayback
│ │ ├── defaultPlaybackState.ts
│ │ ├── index.ts
│ │ ├── mediaPlayback.ts
│ │ ├── mediaPlaybackSettings.ts
│ │ ├── mediaPlayer.ts
│ │ ├── miniPlayer.ts
│ │ ├── miniPlayerRemote.ts
│ │ ├── playback.ts
│ │ ├── players
│ │ │ ├── DualAudioPlayer.ts
│ │ │ ├── HLSPlayer.ts
│ │ │ ├── HTML5Player.ts
│ │ │ ├── OmniPlayer.ts
│ │ │ ├── hlsMetadataPlayer.ts
│ │ │ ├── icecastPlayer.ts
│ │ │ └── observeNearEnd.ts
│ │ ├── scrobbler.ts
│ │ └── visualizerPlayer.ts
│ ├── mediaServices
│ │ ├── all.ts
│ │ ├── index.ts
│ │ ├── mediaServices.ts
│ │ ├── noAuth.ts
│ │ └── servicesSettings.ts
│ ├── metadata
│ │ ├── index.ts
│ │ ├── matcher.ts
│ │ ├── metadata.ts
│ │ ├── metadataChanges.ts
│ │ ├── music-metadata-js.ts
│ │ ├── playlistParser.ts
│ │ ├── thumbnails.ts
│ │ └── userData.ts
│ ├── mixcloud
│ │ ├── index.ts
│ │ ├── mixcloud.ts
│ │ ├── mixcloudApi.ts
│ │ ├── mixcloudPlayer.ts
│ │ └── types.d.ts
│ ├── musicbrainz
│ │ ├── MusicBrainzAlbumTracksPager.ts
│ │ ├── coverart.ts
│ │ ├── digitalFormats.ts
│ │ ├── index.ts
│ │ ├── musicbrainzApi.ts
│ │ └── types.d.ts
│ ├── navidrome
│ │ ├── NavidromePager.ts
│ │ ├── components
│ │ │ ├── NavidromeLoginDialog.tsx
│ │ │ └── NavidromeServerSettings.tsx
│ │ ├── index.ts
│ │ ├── navidrome.ts
│ │ ├── navidromeApi.ts
│ │ ├── navidromeAuth.ts
│ │ ├── navidromeSettings.ts
│ │ ├── subsonicApi.ts
│ │ └── types.d.ts
│ ├── online.ts
│ ├── pagers
│ │ ├── AbstractPager.ts
│ │ ├── ErrorPager.ts
│ │ ├── OffsetPager.ts
│ │ ├── SequentialPager.ts
│ │ ├── SimpleMediaPager.ts
│ │ ├── SimplePager.ts
│ │ ├── SubjectPager.ts
│ │ ├── WrappedPager.ts
│ │ ├── fetchAllTracks.ts
│ │ └── fetchFirstPage.ts
│ ├── pins
│ │ └── pinStore.ts
│ ├── playlist
│ │ ├── index.ts
│ │ └── playlist.ts
│ ├── plex
│ │ ├── PlexPager.ts
│ │ ├── bootstrap.ts
│ │ ├── components
│ │ │ ├── PlexHost.tsx
│ │ │ ├── PlexLogin.tsx
│ │ │ ├── PlexServerSettings.tsx
│ │ │ ├── usePinRefresher.ts
│ │ │ └── usePlexMediaServers.ts
│ │ ├── index.ts
│ │ ├── plex.ts
│ │ ├── plexApi.ts
│ │ ├── plexAuth.ts
│ │ ├── plexItemType.ts
│ │ ├── plexMediaType.ts
│ │ ├── plexRadioPlayer.ts
│ │ ├── plexReporting.ts
│ │ ├── plexScrobbler.ts
│ │ ├── plexSettings.ts
│ │ ├── plexUtils.ts
│ │ └── types.d.ts
│ ├── preferences.ts
│ ├── recentPlaylists.ts
│ ├── reporting.ts
│ ├── scrobbleSettings.ts
│ ├── session.ts
│ ├── soundcloud
│ │ ├── index.ts
│ │ ├── soundcloud.ts
│ │ ├── soundcloudApi.ts
│ │ └── soundcloudPlayer.ts
│ ├── spotify
│ │ ├── SpotifyPager.ts
│ │ ├── bootstrap.ts
│ │ ├── components
│ │ │ ├── SpotifyCredentials.tsx
│ │ │ ├── SpotifyLogin.tsx
│ │ │ └── useCredentials.ts
│ │ ├── index.ts
│ │ ├── samplePitches.ts
│ │ ├── spotify.ts
│ │ ├── spotifyApi.ts
│ │ ├── spotifyAudioAnalyser.ts
│ │ ├── spotifyAuth.ts
│ │ ├── spotifyPlayer.ts
│ │ └── spotifySettings.ts
│ ├── subsonic
│ │ ├── airsonic.ts
│ │ ├── ampache.ts
│ │ ├── factory
│ │ │ ├── SubsonicApi.ts
│ │ │ ├── SubsonicAuth.ts
│ │ │ ├── SubsonicPager.ts
│ │ │ ├── SubsonicService.ts
│ │ │ ├── SubsonicSettings.ts
│ │ │ ├── components
│ │ │ │ ├── SubsonicLoginDialog.scss
│ │ │ │ └── SubsonicLoginDialog.tsx
│ │ │ └── subsonicScrobbler.ts
│ │ ├── gonic.ts
│ │ ├── index.ts
│ │ ├── subsonic.ts
│ │ └── types.d.ts
│ ├── theme
│ │ ├── fonts.ts
│ │ ├── index.ts
│ │ ├── theme.ts
│ │ ├── themeStore.ts
│ │ └── themes
│ │ │ ├── astronaut.json
│ │ │ ├── blackgold.json
│ │ │ ├── carbon.json
│ │ │ ├── contrast.json
│ │ │ ├── debug.json
│ │ │ ├── default.json
│ │ │ ├── glacier.json
│ │ │ ├── index.ts
│ │ │ ├── indigo.json
│ │ │ ├── jeep.json
│ │ │ ├── lego.json
│ │ │ ├── mellowyellow.json
│ │ │ ├── moodyblue.json
│ │ │ ├── neon.json
│ │ │ ├── notebook.json
│ │ │ ├── palepink.json
│ │ │ ├── potpourri.json
│ │ │ ├── proton.json
│ │ │ ├── purplelicious.json
│ │ │ ├── radioactive.json
│ │ │ ├── saddle.json
│ │ │ ├── treasure.json
│ │ │ ├── velvet.json
│ │ │ ├── winamp-classic.json
│ │ │ └── winamp-modern.json
│ ├── tidal
│ │ ├── TidalPager.ts
│ │ ├── bootstrap.ts
│ │ ├── components
│ │ │ ├── TidalCredentials.tsx
│ │ │ ├── TidalLogin.tsx
│ │ │ └── useCredentials.ts
│ │ ├── index.ts
│ │ ├── tidal.ts
│ │ ├── tidalApi.ts
│ │ ├── tidalAuth.ts
│ │ ├── tidalPlayer.ts
│ │ └── tidalSettings.ts
│ ├── visualizer
│ │ ├── AbstractVisualizerPlayer.ts
│ │ ├── ambientvideo
│ │ │ ├── AmbientVideoPlayer.ts
│ │ │ ├── ambientvideo.ts
│ │ │ ├── index.ts
│ │ │ └── visualizers
│ │ │ │ ├── defaultAmbientVideos.ts
│ │ │ │ └── index.ts
│ │ ├── ampshader
│ │ │ ├── AmpShaderPlayer.ts
│ │ │ ├── ampshader.ts
│ │ │ ├── footer.frag
│ │ │ ├── header.frag
│ │ │ ├── index.ts
│ │ │ └── visualizers
│ │ │ │ ├── 20221105_inercia.frag
│ │ │ │ ├── 25boxes.frag
│ │ │ │ ├── 3dAudioVisualizer2.frag
│ │ │ │ ├── abstractMusic.frag
│ │ │ │ ├── abstract_audio_react.frag
│ │ │ │ ├── allNight.frag
│ │ │ │ ├── ambilight.frag
│ │ │ │ ├── attemptAtVdjEffects.frag
│ │ │ │ ├── audioFlightV2.frag
│ │ │ │ ├── audioPulsar.frag
│ │ │ │ ├── audioReactiveFractal.frag
│ │ │ │ ├── audioReactiveScene1.frag
│ │ │ │ ├── audioSpectrum.frag
│ │ │ │ ├── audioVisualizer.frag
│ │ │ │ ├── audioVizWavePsyTrance.frag
│ │ │ │ ├── audioWaveformVisualizerV4.frag
│ │ │ │ ├── av4Lasers.frag
│ │ │ │ ├── avRainbow.frag
│ │ │ │ ├── avRaymarching.frag
│ │ │ │ ├── barebones.frag
│ │ │ │ ├── basicAudioVisualizerModified.frag
│ │ │ │ ├── beatOfBrokenHearts.frag
│ │ │ │ ├── brokowi .frag
│ │ │ │ ├── bubbles.frag
│ │ │ │ ├── burnSoundWave.frag
│ │ │ │ ├── carelsAudioVisualizer.frag
│ │ │ │ ├── cavitation.frag
│ │ │ │ ├── chromaticResonance.frag
│ │ │ │ ├── circuits.frag
│ │ │ │ ├── cityAtNight.frag
│ │ │ │ ├── clairDeLune.frag
│ │ │ │ ├── convertedPlasma.frag
│ │ │ │ ├── creation.frag
│ │ │ │ ├── cylonsJam.frag
│ │ │ │ ├── dancingDots.frag
│ │ │ │ ├── dancingGlowLights.frag
│ │ │ │ ├── dancingJellyfish.frag
│ │ │ │ ├── dancingOctopus.frag
│ │ │ │ ├── disco2000.frag
│ │ │ │ ├── diveIntoGeometry.frag
│ │ │ │ ├── eyeOfHajiSauron.frag
│ │ │ │ ├── fft-ifs.frag
│ │ │ │ ├── firstShaderTest.frag
│ │ │ │ ├── firstVisualiser_globaldusk.frag
│ │ │ │ ├── forkFractal77.frag
│ │ │ │ ├── forkSoundEclipNobody012.frag
│ │ │ │ ├── forkSoundEclipReverland340.frag
│ │ │ │ ├── forkWaves.frag
│ │ │ │ ├── fractalAudio01.frag
│ │ │ │ ├── fractalBrownian.frag
│ │ │ │ ├── fractalLand.frag
│ │ │ │ ├── gameboy.frag
│ │ │ │ ├── gatoNegroPasa.frag
│ │ │ │ ├── gauges.frag
│ │ │ │ ├── goatranceTrip.frag
│ │ │ │ ├── hilbertColor.frag
│ │ │ │ ├── index.ts
│ │ │ │ ├── io.frag
│ │ │ │ ├── issues.frag
│ │ │ │ ├── lelabah.frag
│ │ │ │ ├── lightningStorm.frag
│ │ │ │ ├── mandelKoch.frag
│ │ │ │ ├── mellowRainbowBlob.frag
│ │ │ │ ├── morph.frag
│ │ │ │ ├── musicMandelBoxColour.frag
│ │ │ │ ├── musicVisualiser.frag
│ │ │ │ ├── musicVisualizer3.frag
│ │ │ │ ├── music_Spheres.frag
│ │ │ │ ├── musicalDandelions.frag
│ │ │ │ ├── myLightShow.frag
│ │ │ │ ├── nanoKontrol2.frag
│ │ │ │ ├── nautilus.frag
│ │ │ │ ├── neonPyramid.frag
│ │ │ │ ├── neonRiverVisualizer.frag
│ │ │ │ ├── noiseNoiseRaymarching.frag
│ │ │ │ ├── oscEqualizer.frag
│ │ │ │ ├── otherworldy.frag
│ │ │ │ ├── particlesDance.frag
│ │ │ │ ├── plasmaGlobe.frag
│ │ │ │ ├── playingAroundWithSpirals.frag
│ │ │ │ ├── popShift.frag
│ │ │ │ ├── presets.ts
│ │ │ │ ├── psychedelicEye.frag
│ │ │ │ ├── psychedelicLines.frag
│ │ │ │ ├── purpleSpaghetti.frag
│ │ │ │ ├── radialSoundVisualizer.frag
│ │ │ │ ├── rainbow.frag
│ │ │ │ ├── russianRoulette.frag
│ │ │ │ ├── sailingBeyond.frag
│ │ │ │ ├── shadowDancing.frag
│ │ │ │ ├── shimmy.frag
│ │ │ │ ├── simpleRainbow.frag
│ │ │ │ ├── skulls.frag
│ │ │ │ ├── soapBubble.frag
│ │ │ │ ├── solarDance.frag
│ │ │ │ ├── solumObject.frag
│ │ │ │ ├── soundEclipseRpm.frag
│ │ │ │ ├── soundSinusWave.frag
│ │ │ │ ├── spaceWithMusic.frag
│ │ │ │ ├── spaceshipConsole.frag
│ │ │ │ ├── spaghettis.frag
│ │ │ │ ├── speaker.frag
│ │ │ │ ├── symmetricalSoundVisualiser.frag
│ │ │ │ ├── technoCore.frag
│ │ │ │ ├── test.frag
│ │ │ │ ├── test2.frag
│ │ │ │ ├── uctumi.frag
│ │ │ │ ├── violentEyeSimulator314.frag
│ │ │ │ ├── voyager.frag
│ │ │ │ ├── wavesRemix.frag
│ │ │ │ └── yearOfTruchets018.frag
│ │ ├── audiomotion
│ │ │ ├── AudioMotionPlayer.ts
│ │ │ ├── audiomotion.ts
│ │ │ ├── index.ts
│ │ │ └── visualizers
│ │ │ │ └── index.ts
│ │ ├── butterchurn
│ │ │ ├── ButterchurnPlayer.ts
│ │ │ ├── butterchurn.ts
│ │ │ ├── index.ts
│ │ │ ├── types.d.ts
│ │ │ └── visualizers
│ │ │ │ └── index.ts
│ │ ├── coverart
│ │ │ ├── AnimatedBackgroundPlayer.ts
│ │ │ ├── CovertArtPlayer.ts
│ │ │ ├── animatedBackground.frag
│ │ │ ├── components
│ │ │ │ ├── CoverArtVisualizer.scss
│ │ │ │ ├── CoverArtVisualizer.tsx
│ │ │ │ ├── CurrentlyPlaying.tsx
│ │ │ │ └── useNextTrack.ts
│ │ │ ├── coverart.ts
│ │ │ ├── index.ts
│ │ │ └── visualizers
│ │ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── spotifyviz
│ │ │ ├── SpotifyVizPlayer.ts
│ │ │ ├── index.ts
│ │ │ ├── spotifyviz.ts
│ │ │ └── visualizers
│ │ │ │ ├── example.ts
│ │ │ │ └── index.ts
│ │ ├── visualizer.ts
│ │ ├── visualizerProviders.ts
│ │ ├── visualizerSettings.ts
│ │ ├── visualizerStore.ts
│ │ ├── visualizers.ts
│ │ └── waveform
│ │ │ ├── BeatsPlayer.ts
│ │ │ ├── WaveformPlayer.ts
│ │ │ ├── index.ts
│ │ │ ├── visualizers
│ │ │ ├── index.ts
│ │ │ └── test.ts
│ │ │ └── waveform.ts
│ └── youtube
│ │ ├── YouTubePager.ts
│ │ ├── YouTubePlayer.ts
│ │ ├── YouTubePlaylistLoader.ts
│ │ ├── components
│ │ ├── YouTubeCredentials.tsx
│ │ ├── YouTubeLogin.tsx
│ │ ├── useCredentials.ts
│ │ └── useGoogleClientLibrary.ts
│ │ ├── index.ts
│ │ ├── youtube.ts
│ │ ├── youtubeApi.ts
│ │ ├── youtubeAuth.ts
│ │ ├── youtubeCache.ts
│ │ └── youtubeSettings.ts
├── styles
│ ├── _effects.scss
│ ├── base.scss
│ ├── forms.scss
│ ├── index.scss
│ ├── libs
│ │ └── minireset.scss
│ ├── reset.scss
│ └── theming.scss
├── types
│ ├── Action.ts
│ ├── AmpcastElectron.d.ts
│ ├── AudioManager.d.ts
│ ├── AudioSettings.d.ts
│ ├── Auth.d.ts
│ ├── BackupFile.d.ts
│ ├── BaseMediaObject.d.ts
│ ├── BaseMediaService.d.ts
│ ├── BaseVisualizer.d.ts
│ ├── Browsable.d.ts
│ ├── BuildConfig.d.ts
│ ├── ChildOf.d.ts
│ ├── CreatePlaylistOptions.d.ts
│ ├── DRMInfo.d.ts
│ ├── DRMKeySystem.d.ts
│ ├── DRMType.d.ts
│ ├── DataService.ts
│ ├── ErrorReport.d.ts
│ ├── FilterType.ts
│ ├── ItemType.ts
│ ├── ItemsByService.d.ts
│ ├── LibraryAction.d.ts
│ ├── LinearType.ts
│ ├── Listen.d.ts
│ ├── LookupStatus.ts
│ ├── MediaAlbum.d.ts
│ ├── MediaArtist.d.ts
│ ├── MediaFilter.d.ts
│ ├── MediaFolder.d.ts
│ ├── MediaFolderItem.d.ts
│ ├── MediaItem.d.ts
│ ├── MediaObject.d.ts
│ ├── MediaPlayback.d.ts
│ ├── MediaPlaylist.d.ts
│ ├── MediaSearchParams.d.ts
│ ├── MediaService.d.ts
│ ├── MediaServiceId.d.ts
│ ├── MediaSource.d.ts
│ ├── MediaSourceLayout.d.ts
│ ├── MediaType.ts
│ ├── MetadataChange.d.ts
│ ├── NextVisualizerReason.d.ts
│ ├── Pager.d.ts
│ ├── ParentOf.d.ts
│ ├── PersonalMediaLibrary.d.ts
│ ├── PersonalMediaServerSettings.d.ts
│ ├── PersonalMediaService.d.ts
│ ├── Pin.d.ts
│ ├── PlayAction.d.ts
│ ├── PlayableItem.d.ts
│ ├── Playback.d.ts
│ ├── PlaybackState.d.ts
│ ├── PlaybackType.ts
│ ├── Player.d.ts
│ ├── Playlist.d.ts
│ ├── PlaylistItem.d.ts
│ ├── Preferences.d.ts
│ ├── PublicMediaService.d.ts
│ ├── ReplayGainMode.d.ts
│ ├── Report.d.ts
│ ├── ServiceType.ts
│ ├── Snapshot.d.ts
│ ├── Theme.d.ts
│ ├── Thumbnail.d.ts
│ ├── UserData.d.ts
│ ├── UserTheme.d.ts
│ ├── Visualizer.d.ts
│ ├── VisualizerFavorite.d.ts
│ ├── VisualizerProvider.d.ts
│ ├── VisualizerProviderId.d.ts
│ ├── VisualizerSettings.d.ts
│ └── global.d.ts
└── utils
│ ├── LiteStorage
│ ├── LiteStorage.ts
│ ├── index.ts
│ └── memoryStorage.ts
│ ├── Logger.ts
│ ├── RateLimiter.ts
│ ├── array.ts
│ ├── browser.ts
│ ├── date.ts
│ ├── dom.ts
│ ├── event.ts
│ ├── fetch.ts
│ ├── index.ts
│ ├── media.ts
│ ├── number.ts
│ ├── string.ts
│ └── utils.ts
├── tsconfig.json
└── webpack.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | # ignore everything
2 | *
3 |
4 | # except:
5 | !src
6 | !src/**
7 |
--------------------------------------------------------------------------------
/.github/workflows/test-castlabs.yml:
--------------------------------------------------------------------------------
1 | name: Test castlabs integration
2 |
3 | on: workflow_dispatch
4 |
5 | jobs:
6 | test:
7 | name: Test castlabs integration (windows)
8 | runs-on: windows-latest
9 | timeout-minutes: 10
10 |
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-node@v4
14 | with:
15 | node-version: "20.x"
16 |
17 | - name: Install castlabs-evs
18 | working-directory: ./app
19 | run: python -m pip install --upgrade castlabs-evs
20 |
21 | - name: Test castlabs-evs
22 | working-directory: ./app
23 | run: python -m castlabs_evs.account --version
24 |
25 | - name: Authorize
26 | working-directory: ./app
27 | run: python -m castlabs_evs.account reauth -A ${{secrets.CASTLABS_ACCOUNT_NAME}} -P ${{secrets.CASTLABS_PASSWORD}}
28 |
--------------------------------------------------------------------------------
/.github/workflows/test-notarytool.yml:
--------------------------------------------------------------------------------
1 | name: Test mac notarytool
2 |
3 | on: workflow_dispatch
4 |
5 | jobs:
6 | test:
7 | name: Run mac notarytool
8 | runs-on: macos-latest
9 | timeout-minutes: 10
10 |
11 | steps:
12 | - name: notarytool history
13 | run: xcrun notarytool history --apple-id ${{secrets.APPLE_ID}} --password ${{secrets.APPLE_ID_PASSWORD}} --team-id ${{secrets.APPLE_TEAM_ID}}
14 |
15 | #- name: notarytool last invalid run
16 | # run: xcrun notarytool log 59eda425-049f-48f0-ad42-0524b1cdc78f --apple-id ${{secrets.APPLE_ID}} --password ${{secrets.APPLE_ID_PASSWORD}} --team-id ${{secrets.APPLE_TEAM_ID}}
17 |
18 | #- name: notarytool first inprogress run
19 | # run: xcrun notarytool log 6d75594c-f8d3-445c-903c-9e18f40e7ae6 --apple-id ${{secrets.APPLE_ID}} --password ${{secrets.APPLE_ID_PASSWORD}} --team-id ${{secrets.APPLE_TEAM_ID}}
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | node_modules
4 | www-dev
5 | .env
6 | .env.*
7 | !.env.example
8 | stats.json
9 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | node_modules
4 | dist
5 | www/index.html
6 | www/*.js
7 | www/*.css
8 | www/*.zip
9 | www/v*/*
10 |
--------------------------------------------------------------------------------
/app/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "ampcast",
4 | "asar",
5 | "castlabs",
6 | "Emby",
7 | "Jellyfin",
8 | "ListenBrainz",
9 | "Milkdrop",
10 | "Navidrome",
11 | "portfinder",
12 | "rekkyrosso",
13 | "togglefullscreen",
14 | "Winamp",
15 | "wvcus"
16 | ]
17 | }
--------------------------------------------------------------------------------
/app/assets/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-jit
6 |
7 | com.apple.security.cs.allow-unsigned-executable-memory
8 |
9 | com.apple.security.cs.allow-dyld-environment-variables
10 |
11 | com.apple.security.cs.disable-library-validation
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/assets/icons/ampcast-grayscale.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/assets/icons/ampcast-grayscale.png
--------------------------------------------------------------------------------
/app/assets/icons/ampcast.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/assets/icons/ampcast.ico
--------------------------------------------------------------------------------
/app/assets/icons/ampcast.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/assets/icons/ampcast.png
--------------------------------------------------------------------------------
/app/assets/icons/ampcast.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/assets/icons/ampcast128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/assets/icons/ampcast128x128.png
--------------------------------------------------------------------------------
/app/assets/icons/ampcast16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/assets/icons/ampcast16x16.png
--------------------------------------------------------------------------------
/app/assets/icons/ampcast256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/assets/icons/ampcast256x256.png
--------------------------------------------------------------------------------
/app/assets/icons/ampcast32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/assets/icons/ampcast32x32.png
--------------------------------------------------------------------------------
/app/assets/icons/ampcast64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/assets/icons/ampcast64x64.png
--------------------------------------------------------------------------------
/app/assets/icons/setup.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/assets/icons/setup.gif
--------------------------------------------------------------------------------
/app/assets/icons/setup.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/assets/icons/setup.ico
--------------------------------------------------------------------------------
/app/assets/icons/setup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/assets/icons/setup.png
--------------------------------------------------------------------------------
/app/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | singleQuote: true,
3 | tabWidth: 4,
4 | printWidth: 100,
5 | endOfLine: 'lf',
6 | bracketSpacing: false,
7 | };
8 |
--------------------------------------------------------------------------------
/app/scripts/afterPack.js:
--------------------------------------------------------------------------------
1 | exports.default = function (context) {
2 | // Skip if not mac
3 | if (process.platform !== 'darwin') {
4 | return;
5 | }
6 |
7 | // VMP sign via EVS
8 | const {execSync} = require('child_process');
9 | console.log(' • VMP signing start');
10 | execSync('python -m castlabs_evs.vmp sign-pkg ' + context.appOutDir);
11 | console.log(' • VMP signing complete');
12 | };
13 |
--------------------------------------------------------------------------------
/app/scripts/uninstaller.nsh:
--------------------------------------------------------------------------------
1 | !macro customUnInstall
2 | MessageBox MB_YESNO "Remove all data associated with this app?" \
3 | /SD IDNO IDNO Skipped IDYES Accepted
4 |
5 | Accepted:
6 | RMDir /r "$APPDATA\${APP_FILENAME}"
7 | !ifdef APP_PRODUCT_FILENAME
8 | RMDir /r "$APPDATA\${APP_PRODUCT_FILENAME}"
9 | !endif
10 | Goto done
11 | Skipped:
12 | Goto done
13 | done:
14 | !macroend
--------------------------------------------------------------------------------
/app/src/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/src/icon.png
--------------------------------------------------------------------------------
/app/src/preload.js:
--------------------------------------------------------------------------------
1 | const {contextBridge, ipcRenderer} = require('electron');
2 |
3 | contextBridge.exposeInMainWorld('ampcastElectron', {
4 | quit: () => ipcRenderer.send('quit'),
5 | getCredential: (key) => ipcRenderer.invoke('getCredential', key),
6 | setCredential: (key, value) => ipcRenderer.invoke('setCredential', key, value),
7 | clearCredentials: () => ipcRenderer.invoke('clearCredentials'),
8 | setFontSize: (fontSize) => ipcRenderer.send('setFontSize', fontSize),
9 | setFrameColor: (color) => ipcRenderer.send('setFrameColor', color),
10 | setFrameTextColor: (color) => ipcRenderer.send('setFrameTextColor', color),
11 | getPreferredPort: () => ipcRenderer.invoke('getPreferredPort'),
12 | setPreferredPort: (port) => ipcRenderer.invoke('setPreferredPort', port),
13 | });
14 |
--------------------------------------------------------------------------------
/app/src/splash.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
ampcast
4 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/src/splash.png
--------------------------------------------------------------------------------
/app/src/store.js:
--------------------------------------------------------------------------------
1 | const Store = require('electron-store');
2 |
3 | const store = new Store({
4 | port: {type: 'number', default: 0},
5 | });
6 |
7 | module.exports = {
8 | get port() {
9 | return store.get('port');
10 | },
11 |
12 | set port(port) {
13 | store.set('port', Number(port) || 0);
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/app/www/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 404
5 |
6 |
7 | Not Found
8 |
9 |
--------------------------------------------------------------------------------
/app/www/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/www/apple-touch-icon.png
--------------------------------------------------------------------------------
/app/www/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/www/favicon.ico
--------------------------------------------------------------------------------
/app/www/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/www/icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/www/icon-192.png
--------------------------------------------------------------------------------
/app/www/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/www/icon-512.png
--------------------------------------------------------------------------------
/app/www/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ampcast",
3 | "short_name": "ampcast",
4 | "start_url": "/",
5 | "display_override": [
6 | "window-controls-overlay",
7 | "minimal-ui"
8 | ],
9 | "display": "standalone",
10 | "description": "Ampcast music player",
11 | "background_color": "#32312f",
12 | "theme_color": "#32312f",
13 | "icons": [
14 | {
15 | "src": "icon-192.png",
16 | "type": "image/png",
17 | "sizes": "192x192"
18 | },
19 | {
20 | "src": "icon-512.png",
21 | "type": "image/png",
22 | "sizes": "512x512"
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/app/www/preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rekkyrosso/ampcast/36978b327a478ac2b180a3c85b9aa7fb6cebb86c/app/www/preview.jpg
--------------------------------------------------------------------------------
/app/www/privacy-policy.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Privacy Policy
7 |
8 |
9 |
10 | Privacy Policy for ampcast
11 | This app does not track you, and it doesn't collect your data.
12 |
13 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | // PostCSS configuration
2 |
3 | module.exports = () => {
4 | return {
5 | plugins: [
6 | require('autoprefixer')(),
7 | ],
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | singleQuote: true,
3 | tabWidth: 4,
4 | printWidth: 100,
5 | endOfLine: 'lf',
6 | bracketSpacing: false,
7 | };
8 |
--------------------------------------------------------------------------------
/src/assets/pixel.png.base64.ts:
--------------------------------------------------------------------------------
1 | // https://png-pixel.com/
2 | export default `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1H
3 | AwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=`;
4 |
--------------------------------------------------------------------------------
/src/assets/silence.mp3.base64.ts:
--------------------------------------------------------------------------------
1 | // https://gist.github.com/novwhisky/8a1a0168b94f3b6abfaa#gistcomment-1551393
2 | export default `data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2L
3 | jEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDA
4 | wMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6ur
5 | q6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJA
6 | AAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAA
7 | AGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gA
8 | AAAANVVV`;
9 |
--------------------------------------------------------------------------------
/src/components/Actions/AddToPlaylistDialog.scss:
--------------------------------------------------------------------------------
1 | .add-to-playlist-dialog {
2 | width: 28em;
3 |
4 | .select-service {
5 | display: flex;
6 | align-items: center;
7 |
8 | select {
9 | flex: auto;
10 | }
11 | }
12 |
13 | .playlists {
14 | position: relative;
15 | height: 14em;
16 | width: 100%;
17 | inset: auto;
18 | }
19 |
20 | .scrollable {
21 | --scrollbar-size: max(10px, calc(1.25em * var(--scrollbar-thickness)));
22 | --scrollbar-color-hi: var(--scrollbar-color);
23 | --scrollbar-color-lo: var(--scrollbar-color);
24 | }
25 |
26 | .status-bar {
27 | display: none;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Actions/CreatePlaylistDialog.scss:
--------------------------------------------------------------------------------
1 | .create-playlist-dialog {
2 | textarea {
3 | min-width: 13.25em;
4 | }
5 |
6 | &.service-emby &-public label,
7 | &.service-jellyfin &-public label,
8 | &.service-plex &-public label {
9 | opacity: 0.5;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Actions/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './Actions';
2 | export * from './Actions';
3 | export * from './PlaylistActions';
4 | export {default as performAction} from './performAction';
5 |
--------------------------------------------------------------------------------
/src/components/Actions/useEditablePlaylistsPager.ts:
--------------------------------------------------------------------------------
1 | import MediaPlaylist from 'types/MediaPlaylist';
2 | import MediaService from 'types/MediaService';
3 | import Pager from 'types/Pager';
4 | import useSource from 'hooks/useSource';
5 |
6 | export default function useEditablePlaylistsPager(
7 | service: MediaService | null
8 | ): Pager | null {
9 | return useSource(service?.editablePlaylists || null);
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/App/AppDragRegion.scss:
--------------------------------------------------------------------------------
1 | .app-drag-region {
2 | position: fixed;
3 | top: 0;
4 | width: 100%;
5 | height: 1.5rem;
6 | -webkit-app-region: drag;
7 | user-select: none;
8 | display: none;
9 |
10 | @mixin chromeless {
11 | display: block;
12 | }
13 |
14 | @media (display-mode: window-controls-overlay) {
15 | @include chromeless;
16 | }
17 |
18 | .electron & {
19 | @include chromeless;
20 | height: env(titlebar-area-height, 1.5rem);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/App/AppDragRegion.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {preventDefault} from 'utils';
3 | import './AppDragRegion.scss';
4 |
5 | export default function AppDragRegion() {
6 | return
;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/App/AppTitle.scss:
--------------------------------------------------------------------------------
1 | .app-title {
2 | font-family: Arial, sans-serif; /* independent of theme */
3 |
4 | .app-icon {
5 | margin-right: 0.25em;
6 | }
7 |
8 | .app-version {
9 | margin-left: 0.25em;
10 | padding: 0.125em 0.3125em;
11 | font-size: max(0.5625em, 8px);
12 | vertical-align: super;
13 | background-color: var(--frame2-color);
14 | border-radius: var(--app-border-radius);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/App/AppTitle.tsx:
--------------------------------------------------------------------------------
1 | import React, {memo} from 'react';
2 | import Icon from 'components/Icon';
3 | import './AppTitle.scss';
4 |
5 | export default memo(function AppTitle() {
6 | return (
7 |
8 |
9 |
10 | {__app_name__}
11 | {' '}
12 | {__app_version__}
13 |
14 | );
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/App/DesktopWarning.scss:
--------------------------------------------------------------------------------
1 | .desktop-warning {
2 | position: absolute;
3 | inset: 0;
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | justify-content: center;
8 | text-align: center;
9 | padding: 2rem;
10 | background-color: var(--background-color);
11 | color: var(--text-color);
12 | font-size: 1.5rem;
13 |
14 | .app-title {
15 | margin-bottom: 4rem;
16 | font-size: 1rem;
17 | }
18 |
19 | p + p {
20 | margin-top: 5rem;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/App/DesktopWarning.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AppTitle from './AppTitle';
3 | import './DesktopWarning.scss';
4 |
5 | export interface DesktopWarningProps {
6 | onDismiss: () => void;
7 | }
8 |
9 | export default function DesktopWarning({onDismiss}: DesktopWarningProps) {
10 | return (
11 |
12 |
13 |
This application is intended for a desktop browser.
14 |
15 | Proceed
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/App/PortUnavailable.scss:
--------------------------------------------------------------------------------
1 | .port-unavailable {
2 | font-size: min(3vh, 24px);
3 | padding: 2em;
4 |
5 | .app-title {
6 | margin: 0;
7 | }
8 |
9 | h2 {
10 | margin: 1em 0;
11 | font-weight: bold;
12 | font-size: 1.125em;
13 | }
14 |
15 | p + p {
16 | margin-top: 1em;
17 | }
18 |
19 | ul {
20 | margin-top: 0.5em;
21 |
22 | & + p {
23 | margin-top: 2em;
24 | }
25 | }
26 |
27 | li {
28 | line-height: 1.8;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/App/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './App';
2 | export * from './App';
3 |
--------------------------------------------------------------------------------
/src/components/App/useAppSettings.ts:
--------------------------------------------------------------------------------
1 | import {useEffect} from 'react';
2 | import {observePreferences} from 'services/preferences';
3 |
4 | export default function useAppSettings(): void {
5 | useEffect(() => {
6 | const subscription = observePreferences().subscribe(
7 | ({disableExplicitContent, markExplicitContent}) => {
8 | const {classList} = document.body;
9 | classList.toggle('disable-explicit-content', disableExplicitContent);
10 | classList.toggle('mark-explicit-content', markExplicitContent);
11 | }
12 | );
13 | return () => subscription.unsubscribe();
14 | }, []);
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/App/useBrowser.ts:
--------------------------------------------------------------------------------
1 | import {useEffect} from 'react';
2 | import {browser} from 'utils';
3 |
4 | export default function useBrowser(): void {
5 | useEffect(() => {
6 | const classList = document.body.classList;
7 | classList.add(`browser-${browser.name.replace(/\s+/g, '-')}`);
8 | if (browser.isElectron) {
9 | classList.add('electron');
10 | if (browser.os === 'Mac OS') {
11 | classList.add('electron-mac');
12 | }
13 | }
14 | }, []);
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/App/usePreload.ts:
--------------------------------------------------------------------------------
1 | import {useEffect} from 'react';
2 | import {isMiniPlayer} from 'utils';
3 | import {loadMediaServices} from 'services/mediaServices';
4 | import {loadVisualizers} from 'services/visualizer/visualizerProviders';
5 |
6 | export default function usePreload(): void {
7 | useEffect(() => {
8 | loadMediaServices();
9 | if (isMiniPlayer) {
10 | loadVisualizers();
11 | }
12 | }, []);
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Badges/Badges.scss:
--------------------------------------------------------------------------------
1 | .badges {
2 | display: flex;
3 | flex-direction: row;
4 | align-items: center;
5 | font-size: max(0.5em, 9px);
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Badges/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './Badges';
2 | export * from './Badges';
3 | export * from './Badge';
4 |
--------------------------------------------------------------------------------
/src/components/Button/CopyButton.scss:
--------------------------------------------------------------------------------
1 | .copy-button {
2 | display: inline-flex;
3 | align-items: center;
4 | text-align: left;
5 | font-size: max(0.75em, 10px);
6 | padding: 0.25em 1em;
7 | min-width: 7em;
8 | cursor: pointer;
9 |
10 | .icon {
11 | margin-right: 0.5em;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Button/IconButton.scss:
--------------------------------------------------------------------------------
1 | .icon-button {
2 | all: unset;
3 | display: inline-flex;
4 | flex-direction: column;
5 | align-items: center;
6 | justify-content: center;
7 | text-align: center;
8 | width: 1em;
9 | height: 1em;
10 | opacity: 0.75;
11 | border-radius: 50%;
12 |
13 | &:disabled {
14 | color: var(--grey1-color);
15 | }
16 |
17 | &:enabled {
18 | cursor: pointer;
19 |
20 | .focus-visible &:focus,
21 | &:hover {
22 | opacity: 1;
23 | }
24 | }
25 |
26 | &:active {
27 | transform: none;
28 | }
29 |
30 | .icon {
31 | width: inherit;
32 | height: inherit;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/Button/IconButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Except} from 'type-fest';
3 | import Icon, {IconName} from 'components/Icon';
4 | import {cancelEvent, stopPropagation} from 'utils';
5 | import './IconButton.scss';
6 |
7 | export interface IconButtonProps
8 | extends Except, 'children'> {
9 | icon: IconName;
10 | }
11 |
12 | export default function IconButton({icon, className = '', ...props}: IconButtonProps) {
13 | return (
14 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Button/IconButtons.scss:
--------------------------------------------------------------------------------
1 | .icon-buttons {
2 | display: flex;
3 | flex-direction: row;
4 | align-items: center;
5 | padding: 1px;
6 |
7 | .icon-button {
8 | & ~ .icon-button {
9 | margin-left: 0.25em;
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Button/IconButtons.tsx:
--------------------------------------------------------------------------------
1 | import React, {HTMLAttributes} from 'react';
2 | import {cancelEvent} from 'utils';
3 | import './IconButtons.scss';
4 |
5 | export type IconButtonsProps = HTMLAttributes;
6 |
7 | export default function IconButtons({className = '', children, ...props}: IconButtonsProps) {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Button/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './IconButton';
2 | export * from './IconButton';
3 |
--------------------------------------------------------------------------------
/src/components/CoverArt/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './CoverArt';
2 | export * from './CoverArt';
3 |
--------------------------------------------------------------------------------
/src/components/CoverArtVisualizer/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './CoverArtVisualizer';
2 | export * from './CoverArtVisualizer';
3 |
--------------------------------------------------------------------------------
/src/components/DatePicker/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './DatePicker';
2 | export * from './DatePicker';
3 |
--------------------------------------------------------------------------------
/src/components/Dialog/DialogButtons.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export interface DialogButtonsProps {
4 | value?: string;
5 | disabled?: boolean;
6 | submitText?: React.ReactNode;
7 | }
8 |
9 | export default function DialogButtons({
10 | value = '',
11 | disabled,
12 | submitText = 'Confirm',
13 | }: DialogButtonsProps) {
14 | return (
15 |
16 |
17 | Cancel
18 |
19 |
20 | {submitText}
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Dialog/alert.scss:
--------------------------------------------------------------------------------
1 | .alert-dialog {
2 | text-align: center;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/Dialog/confirm.scss:
--------------------------------------------------------------------------------
1 | .confirm-dialog {
2 | text-align: center;
3 |
4 | &-storage {
5 | display: flex;
6 | flex-direction: row;
7 | align-items: center;
8 | justify-content: center;
9 | margin-top: 2em;
10 | font-size: 0.75em;
11 | font-style: italic;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Dialog/error.tsx:
--------------------------------------------------------------------------------
1 | import alert, {AlertOptions} from './alert';
2 |
3 | export default function error(err: Error): Promise;
4 | export default function error(err: string): Promise;
5 | export default function error(err: AlertOptions & {system?: boolean}): Promise;
6 | export default async function error(
7 | err: Error | string | (AlertOptions & {system?: boolean})
8 | ): Promise {
9 | if (typeof err === 'string') {
10 | err = Error(err);
11 | }
12 | const {title = 'Error', message = 'Unknown error', system = true} = err as any;
13 | await alert({
14 | icon: 'error',
15 | title,
16 | message,
17 | system,
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Dialog/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './Dialog';
2 | export * from './Dialog';
3 | export {default as alert} from './alert';
4 | export {default as confirm} from './confirm';
5 | export {default as error} from './error';
6 | export {default as prompt} from './prompt';
7 | export {default as showDialog} from './showDialog';
8 |
--------------------------------------------------------------------------------
/src/components/Dialog/prompt.scss:
--------------------------------------------------------------------------------
1 | .prompt-dialog {
2 | text-align: center;
3 |
4 | input[type="url"] {
5 | width: 24em;
6 | }
7 |
8 | textarea {
9 | min-width: 20em;
10 | min-height: 4em;
11 | width: 32em;
12 | height: 12em;
13 | margin-top: 0.5em;
14 | margin-left: 0;
15 | font-family: monospace;
16 | font-size: 0.875em;
17 | }
18 |
19 | label:has(+ textarea) {
20 | display: block;
21 | text-align: left;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/Errors/ErrorBox.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/effects.scss';
2 |
3 | .error-box {
4 | @include effects.inset;
5 | flex: auto;
6 | padding: 1em;
7 | border-radius: var(--app-border-radius);
8 | overflow: hidden;
9 |
10 | h2 {
11 | font-size: 1.5em;
12 | font-weight: bold;
13 | margin-bottom: 0.75em;
14 | }
15 |
16 | .buttons {
17 | margin-top: 1em;
18 | text-align: left;
19 | }
20 |
21 | button.disconnect {
22 | white-space: normal;
23 | }
24 | }
--------------------------------------------------------------------------------
/src/components/Errors/ErrorReport.scss:
--------------------------------------------------------------------------------
1 | .error-report {
2 | font-size: max(0.75em, 1rem);
3 |
4 | pre {
5 | white-space: pre-wrap;
6 |
7 | & + p {
8 | text-align: right;
9 | margin-top: 1em;
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Errors/HandledError.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {NoInternetError} from 'services/errors';
3 | import MediaServiceLabel from 'components/MediaSources/MediaServiceLabel';
4 | import {ErrorBoxProps} from './ErrorBox';
5 |
6 | type HandledErrorProps = ErrorBoxProps & {
7 | error: Error;
8 | };
9 |
10 | export default function HandledError({error, service, children}: HandledErrorProps) {
11 | return (
12 |
13 | {service && error instanceof NoInternetError ? (
14 |
15 |
16 |
17 | ) : null}
18 |
19 |
{error?.message || String(error)}
20 |
21 | {children}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Errors/UnhandledError.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {ErrorBoxProps} from './ErrorBox';
3 | import ErrorReport from './ErrorReport';
4 |
5 | export default function UnhandledError({error, children, reportedBy='MediaBrowser', reportingId, service}: ErrorBoxProps) {
6 | return (
7 |
8 |
Error
9 |
10 | {service && (
11 |
12 |
13 | Reconnect to {service.name}...
14 |
15 |
16 | )}
17 | {children}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/ExternalLink/ExternalLink.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/effects.scss';
2 |
3 | .external-link {
4 | white-space: nowrap;
5 | text-decoration: none;
6 |
7 | &:hover &-text,
8 | &:active &-text {
9 | text-decoration: underline;
10 | }
11 |
12 | &-content {
13 | display: inline-flex;
14 | flex-direction: row;
15 | align-items: center;
16 | justify-content: center;
17 | }
18 |
19 | &-text {
20 | border-radius: calc(var(--roundness) * 0.5em);
21 | outline-offset: 0.125em;
22 |
23 | .icon + & {
24 | margin-left: 0.25em;
25 | }
26 | }
27 |
28 | .focus-visible &:focus &-text {
29 | @include effects.outline;
30 | outline-width: 2px;
31 | }
32 |
33 | > .icon-link {
34 | position: relative;
35 | left: 0.5em;
36 | top: -0.75em;
37 | font-size: 0.625em;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/ExternalLink/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './ExternalLink';
2 | export * from './ExternalLink';
3 |
--------------------------------------------------------------------------------
/src/components/Icon/Flag.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export interface FlagProps {
4 | country: string;
5 | }
6 |
7 | export default function Flag({country}: FlagProps) {
8 | return (
9 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/Icon/SvgDefs.scss:
--------------------------------------------------------------------------------
1 | .svg-defs {
2 | position: absolute;
3 | visibility: hidden;
4 | }
--------------------------------------------------------------------------------
/src/components/Icon/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './Icon';
2 | export * from './Icon';
3 |
--------------------------------------------------------------------------------
/src/components/ListView/DetailsBox.scss:
--------------------------------------------------------------------------------
1 | .details-box {
2 | width: 100%;
3 | height: 10em;
4 | margin: 0.5em 0;
5 |
6 | .list-view-cell.value {
7 | font-family: monospace;
8 | }
9 |
10 | .scrollable {
11 | --scrollbar-size: max(10px, calc(1.25em * var(--scrollbar-thickness)));
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/ListView/ListBox.scss:
--------------------------------------------------------------------------------
1 | .list-box {
2 | width: 100%;
3 | height: calc(11em - 2px);
4 | margin: 0.5em 0;
5 |
6 | .scrollable {
7 | --scrollbar-size: max(10px, calc(1.25em * var(--scrollbar-thickness)));
8 | }
9 |
10 | .list-view-cell {
11 | padding: 0 0.25em;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/ListView/ListBox.tsx:
--------------------------------------------------------------------------------
1 | import React, {useMemo} from 'react';
2 | import {Except} from 'type-fest';
3 | import ListView, {ListViewLayout, ListViewProps} from './ListView';
4 | import './ListBox.scss';
5 |
6 | export interface ListBoxProps extends Except, 'layout'> {
7 | renderItem?: (item: T, rowIndex: number) => React.ReactNode;
8 | }
9 |
10 | export default function ListBox({
11 | renderItem: render = String,
12 | className = '',
13 | ...props
14 | }: ListBoxProps) {
15 | const layout: ListViewLayout = useMemo(() => {
16 | return {view: 'details', cols: [{render}]};
17 | }, [render]);
18 |
19 | return {...props} className={`list-box ${className}`} layout={layout} />;
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/ListView/ListViewBodyCell.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Column} from './ListView';
3 |
4 | export interface ListViewBodyCellProps {
5 | rowIndex: number;
6 | col: Column;
7 | item: T;
8 | }
9 |
10 | export default function ListViewBodyCell({
11 | rowIndex,
12 | col: {className = '', render, style},
13 | item,
14 | }: ListViewBodyCellProps) {
15 | return (
16 |
17 | {render(item, rowIndex)}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/ListView/ListViewHeadCell.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Column} from './ListView';
3 |
4 | export default function ListViewHeadCell({style, title}: Column) {
5 | return (
6 |
7 | {title}
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/ListView/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './ListView';
2 | export * from './ListView';
3 |
--------------------------------------------------------------------------------
/src/components/Login/ConnectionLogging.scss:
--------------------------------------------------------------------------------
1 | .connection-logging {
2 | font-family: monospace;
3 | font-size: 0.75em;
4 | margin-top: 2em;
5 |
6 | p {
7 | margin: 0.25em 0;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Login/CredentialsButton.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback} from 'react';
2 | import MediaService from 'types/MediaService';
3 | import {showCredentialsDialog} from 'components/Settings/MediaLibrarySettings/CredentialsDialog';
4 |
5 | export interface CredentialsButtonProps extends React.ButtonHTMLAttributes {
6 | service: MediaService;
7 | }
8 |
9 | export default function CredentialsButton({
10 | service,
11 | className = 'credentials-button',
12 | children = 'Enter credentials…',
13 | ...props
14 | }: CredentialsButtonProps) {
15 | const handleClick = useCallback(() => {
16 | showCredentialsDialog(service);
17 | }, [service]);
18 |
19 | return (
20 |
21 | {children}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Login/CredentialsRequired.scss:
--------------------------------------------------------------------------------
1 | .credentials-required {
2 | font-size: 0.75em;
3 | padding: 0.5em;
4 |
5 | p {
6 | margin: 0.5em 0;
7 | }
8 |
9 | &-link {
10 | margin-top: 1em;
11 | overflow: hidden;
12 | text-overflow: ellipsis;
13 |
14 | .external-link-content {
15 | display: inline;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Login/CredentialsRequired.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ExternalLink from 'components/ExternalLink';
3 | import {LoginProps} from './Login';
4 | import './CredentialsRequired.scss';
5 |
6 | export default function CredentialsRequired({service}: LoginProps) {
7 | const credentialsUrl = service.credentialsUrl;
8 |
9 | return (
10 |
11 |
Please register a client application to continue.
12 | {credentialsUrl ? (
13 |
14 |
15 |
16 | ) : null}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Login/DefaultLogin.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ConnectionLogging from './ConnectionLogging';
3 | import HTTPDownloadLink from './HTTPDownloadLink';
4 | import LoginButton from './LoginButton';
5 | import ServiceLink from './ServiceLink';
6 | import LoginRequired from './LoginRequired';
7 | import {LoginProps} from './Login';
8 |
9 | export default function DefaultLogin({service}: LoginProps) {
10 | return (
11 | <>
12 |
13 |
14 |
15 |
16 |
17 | >
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Login/HTTPDownloadLink.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ServiceType from 'types/ServiceType';
3 | import {dockerUrl, downloadUrl} from 'services/constants';
4 | import ExternalLink from 'components/ExternalLink';
5 | import {LoginProps} from './Login';
6 |
7 | export default function HTTPDownloadLink({service}: LoginProps) {
8 | const showDownloadLink =
9 | location.protocol === 'https:' &&
10 | service.serviceType === ServiceType.PersonalMedia && service.id !== 'plex';
11 |
12 | return showDownloadLink ? (
13 | <>
14 |
15 | Download the desktop app if you are unable
16 | to login via HTTPS
17 |
18 | or use the docker image .
19 |
20 | >
21 | ) : null;
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/Login/Login.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MediaService from 'types/MediaService';
3 | import DefaultLogin from './DefaultLogin';
4 | import './Login.scss';
5 |
6 | export interface LoginProps {
7 | service: MediaService;
8 | }
9 |
10 | export default function Login({service}: LoginProps) {
11 | const Login = service.Components?.Login || DefaultLogin;
12 | return (
13 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Login/LoginButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MediaService from 'types/MediaService';
3 |
4 | export interface LoginButtonProps extends React.ButtonHTMLAttributes {
5 | service: MediaService;
6 | }
7 |
8 | export default function LoginButton({
9 | service,
10 | children = `Connect to ${service.name}…`,
11 | ...props
12 | }: LoginButtonProps) {
13 | return (
14 |
15 |
16 | {children}
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Login/LoginDialog.scss:
--------------------------------------------------------------------------------
1 | .login-dialog {
2 | input[type=url] {
3 | min-width: 20em;
4 | }
5 |
6 | .message {
7 | text-align: left;
8 | font-size: 0.75em;
9 | min-height: 1.5em;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Login/LoginRequired.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ServiceType from 'types/ServiceType';
3 | import {LoginProps} from './Login';
4 |
5 | export default function LoginRequired({service}: LoginProps) {
6 | return (
7 |
8 | You need to be logged in to{' '}
9 | {service.serviceType === ServiceType.DataService ? 'access your data' : 'play music'} from{' '}
10 | {service.name}.
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Login/RestrictedAccessWarning.scss:
--------------------------------------------------------------------------------
1 | .restricted-access-warning {
2 | font-size: 0.75em;
3 | padding: 0.5em;
4 |
5 | p {
6 | margin: 0.5em 0;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/Login/RestrictedAccessWarning.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {LoginProps} from './Login';
3 | import './RestrictedAccessWarning.scss';
4 |
5 | export default function RestrictedAccessWarning({service}: LoginProps) {
6 | return service.restrictedAccess ? (
7 |
8 |
9 | This application is in development mode .
10 |
11 |
You currently need to be an approved user to access services from {service.name}.
12 |
13 | ) : null;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/Login/ServiceLink.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ExternalLink from 'components/ExternalLink';
3 | import Icon from 'components/Icon';
4 | import {LoginProps} from './Login';
5 |
6 | export default function ServiceLink({service}: LoginProps) {
7 | return (
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Login/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './Login';
2 | export * from './Login';
3 |
--------------------------------------------------------------------------------
/src/components/Media/PlaybackState.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useCurrentTime from 'hooks/useCurrentTime';
3 | import usePaused from 'hooks/usePaused';
4 | import useLoadingState from './useLoadingState';
5 |
6 | export default function PlaybackState() {
7 | const loadingState = useLoadingState();
8 | const paused = usePaused();
9 | const currentTime = useCurrentTime();
10 | const started = currentTime > 0;
11 |
12 | return (
13 |
14 | {loadingState === 'error'
15 | ? 'error'
16 | : paused
17 | ? started
18 | ? 'paused'
19 | : ''
20 | : loadingState === 'loaded'
21 | ? 'playing'
22 | : loadingState}
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Media/ProgressBar.scss:
--------------------------------------------------------------------------------
1 | .progress-bar {
2 | position: absolute;
3 | bottom: 0;
4 | width: 100%;
5 | height: var(--progress-bar-height);
6 | background-color: var(--progress-bar-background-color);
7 | border: 0;
8 | border-radius: 0;
9 | display: none;
10 | transition: opacity 1s linear;
11 |
12 | &[value^="0"] {
13 | opacity: 0;
14 | }
15 |
16 | .media.fullscreen & {
17 | display: block;
18 | }
19 |
20 | &::-webkit-progress-bar {
21 | background-color: var(--progress-bar-background-color);
22 | border-radius: 0;
23 | }
24 |
25 | &::-webkit-progress-value {
26 | background-color: var(--progress-bar-color);
27 | border-radius: 0;
28 | }
29 |
30 | &::-moz-progress-bar {
31 | background-color: var(--progress-bar-color);
32 | border-radius: 0;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/Media/ProgressBar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useCurrentTime from 'hooks/useCurrentTime';
3 | import useDuration from 'hooks/useDuration';
4 | import {MAX_DURATION} from 'services/constants';
5 | import './ProgressBar.scss';
6 |
7 | export default function ProgressBar() {
8 | const duration = useDuration();
9 | const currentTime = useCurrentTime();
10 | return (
11 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/Media/Static.scss:
--------------------------------------------------------------------------------
1 | .static {
2 | /* https://css-tricks.com/making-static-noise-from-a-weird-css-gradient-bug/ */
3 | position: absolute;
4 | inset: 0;
5 | background:
6 | repeating-radial-gradient(#000 0 0.0001%,#fff 0 0.0002%) 50% 0/2500px 2500px,
7 | repeating-conic-gradient(#000 0 0.0001%,#fff 0 0.0002%) 60% 60%/2500px 2500px;
8 | background-blend-mode: difference;
9 | animation: tv-static .2s infinite alternate;
10 | }
11 |
12 | @keyframes tv-static {
13 | 100% {
14 | background-position: 50% 0, 60% 50%
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Media/Static.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Static.scss';
3 |
4 | export default function Static() {
5 | return
;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Media/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './Media';
2 | export * from './Media';
3 |
--------------------------------------------------------------------------------
/src/components/MediaBrowser/ErrorScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ErrorBox, {ErrorBoxProps} from 'components/Errors/ErrorBox';
3 |
4 | export default function ErrorScreen(props: ErrorBoxProps) {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/MediaBrowser/FilterSelect.scss:
--------------------------------------------------------------------------------
1 | .filter-select {
2 | display: flex;
3 | flex-direction: row;
4 | align-items: center;
5 | margin-bottom: var(--gutter-width);
6 |
7 | label {
8 | flex: initial;
9 | margin-left: 0.25em;
10 | }
11 |
12 | select {
13 | flex: auto;
14 | min-width: 0;
15 | margin-right: 1px;
16 | cursor: pointer;
17 | }
18 |
19 | & + .error-box {
20 | margin-bottom: var(--gutter-width);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/MediaBrowser/MediaItems.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MediaItem from 'types/MediaItem';
3 | import MediaItemList from 'components/MediaList/MediaItemList';
4 | import {PagedItemsProps} from './PagedItems';
5 |
6 | export default function MediaItems({source, ...props}: PagedItemsProps) {
7 | return (
8 |
9 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/MediaBrowser/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './MediaBrowser';
2 | export * from './MediaBrowser';
3 |
--------------------------------------------------------------------------------
/src/components/MediaBrowser/useErrorScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, {useMemo} from 'react';
2 | import {FallbackProps} from 'react-error-boundary';
3 | import MediaService from 'types/MediaService';
4 | import {AnyMediaSource} from 'types/MediaSource';
5 | import ErrorScreen from './ErrorScreen';
6 |
7 | export default function useErrorScreen(
8 | service: MediaService,
9 | source: AnyMediaSource
10 | ) {
11 | return useMemo(() => {
12 | return function MediaBrowserError({error}: FallbackProps) {
13 | return ;
14 | };
15 | }, [service, source]);
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/MediaBrowser/useHistoryPager.ts:
--------------------------------------------------------------------------------
1 | import {useMemo} from 'react';
2 | import MediaItem from 'types/MediaItem';
3 | import MediaSource from 'types/MediaSource';
4 | import useSource from 'hooks/useSource';
5 |
6 | export default function useHistoryPager(source: MediaSource | null, startAt = 0) {
7 | const params = useMemo(() => ({startAt}), [startAt]);
8 | const pager = useSource(source, params);
9 | return pager;
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/MediaControls/MediaButton.scss:
--------------------------------------------------------------------------------
1 | .media-button {
2 | background-color: var(--media-button-color);
3 | background-image: var(--media-button-background);
4 | color: var(--media-button-text-color);
5 | border: 0;
6 | padding: 0.5rem;
7 | width: 2rem;
8 | height: 2rem;
9 | font-size: 0.75rem;
10 | display: flex;
11 | flex-direction: row;
12 | align-items: center;
13 | justify-content: center;
14 | box-shadow: 0 0 1px 1px var(--black);
15 | border-radius: calc(var(--roundness) * 100%);
16 |
17 | &:first-child {
18 | margin-left: auto;
19 | }
20 |
21 | &:last-child {
22 | margin-right: auto;
23 | }
24 |
25 | &:enabled:active,
26 | &:enabled.active {
27 | transform: scale(0.98);
28 | }
29 |
30 | &-menu {
31 | width: 1.25rem;
32 | height: 1.25rem;
33 | padding: 0;
34 | cursor: pointer;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/MediaControls/VolumeControl.scss:
--------------------------------------------------------------------------------
1 | .volume-control {
2 | display: flex;
3 | flex: auto;
4 | min-width: 4.25rem;
5 | max-width: 6.25rem;
6 | margin-right: 0.25em;
7 |
8 | .icon-button {
9 | flex: initial;
10 | width: 1.25em;
11 | height: 1.25em;
12 | }
13 |
14 | input[type=range] {
15 | --track-height: 0.15em;
16 | --thumb-size: .875em;
17 | min-width: 3rem;
18 | max-width: 5rem;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/MediaControls/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './MediaControls';
2 | export * from './MediaControls';
3 |
--------------------------------------------------------------------------------
/src/components/MediaInfo/CurrentlyPlaying.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MediaType from 'types/MediaType';
3 | import PlaylistItem from 'types/PlaylistItem';
4 | import Visualizer from 'types/Visualizer';
5 | import MediaInfo from './MediaInfo';
6 | import VisualizerInfo from './VisualizerInfo';
7 |
8 | export interface CurrentlyPlayingProps {
9 | item: PlaylistItem | null;
10 | visualizer: Visualizer | null;
11 | }
12 |
13 | export default function CurrentlyPlaying({item, visualizer}: CurrentlyPlayingProps) {
14 | return (
15 |
16 | {item ?
:
No media loaded.
}
17 | {item && item.mediaType !== MediaType.Video ? (
18 |
19 | ) : null}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/MediaInfo/CurrentlyPlayingTabs.tsx:
--------------------------------------------------------------------------------
1 | import React, {useMemo} from 'react';
2 | import TabList, {TabItem} from 'components/TabList';
3 | import MediaDetails from './MediaDetails';
4 | import CurrentlyPlaying, {CurrentlyPlayingProps} from './CurrentlyPlaying';
5 |
6 | export default function CurrentlyPlayingTabs({item, visualizer}: CurrentlyPlayingProps) {
7 | const tabs: TabItem[] = useMemo(() => {
8 | const tabs = [
9 | {
10 | tab: 'Now playing',
11 | panel: ,
12 | },
13 | ];
14 | if (item) {
15 | tabs.push({
16 | tab: 'Details',
17 | panel: ,
18 | });
19 | }
20 | return tabs;
21 | }, [item, visualizer]);
22 | return ;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/MediaInfo/MediaDetails.scss:
--------------------------------------------------------------------------------
1 | .media-details {
2 | display: flex;
3 | flex-direction: column;
4 | height: 100%;
5 | font-size: max(0.75em, 10px);
6 |
7 | .details-box {
8 | height: 100%;
9 | margin: 0;
10 |
11 | & + p {
12 | margin-top: 1em;
13 | margin-bottom: 0;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/MediaInfo/MediaInfoDialog.scss:
--------------------------------------------------------------------------------
1 | .media-info-dialog {
2 | font-size: min(max(1.5vw, 12px), 1.125em);
3 | width: 40em;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/MediaInfo/MediaInfoTabs.scss:
--------------------------------------------------------------------------------
1 | .media-info-tabs {
2 | .tab-list-tabs {
3 | font-size: 0.875em;
4 | }
5 |
6 | .tab-panel {
7 | position: absolute;
8 | inset: 0;
9 |
10 | &:first-child {
11 | position: static;
12 |
13 | &[hidden] {
14 | display: block;
15 | visibility: hidden;
16 | }
17 | }
18 | }
19 |
20 | & + .dialog-buttons {
21 | margin-top: 1.5em;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/MediaInfo/MediaInfoTabs.tsx:
--------------------------------------------------------------------------------
1 | import React, {useMemo} from 'react';
2 | import MediaObject from 'types/MediaObject';
3 | import TabList, {TabItem} from 'components/TabList';
4 | import MediaInfo, {MediaInfoProps} from './MediaInfo';
5 | import MediaDetails from './MediaDetails';
6 | import './MediaInfoTabs.scss';
7 |
8 | export default function MediaInfoTabs({item}: MediaInfoProps) {
9 | const tabs: TabItem[] = useMemo(
10 | () => [
11 | {
12 | tab: 'General',
13 | panel: ,
14 | },
15 | {
16 | tab: 'Details',
17 | panel: ,
18 | },
19 | ],
20 | [item]
21 | );
22 | return ;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/MediaInfo/useMediaInfoDialog.ts:
--------------------------------------------------------------------------------
1 | import {RefObject, useEffect} from 'react';
2 | import {fromEvent} from 'rxjs';
3 | import {browser} from 'utils';
4 |
5 | export default function useMediaInfoDialog(dialogRef: RefObject) {
6 | useEffect(() => {
7 | const subscription = fromEvent(document, 'keydown', {
8 | capture: true,
9 | }).subscribe((event) => {
10 | if (
11 | event[browser.cmdKey] &&
12 | !event.shiftKey &&
13 | !event.altKey &&
14 | event.code === 'KeyI' &&
15 | !event.repeat
16 | ) {
17 | event.preventDefault();
18 | dialogRef.current?.close();
19 | }
20 | });
21 | return () => subscription.unsubscribe();
22 | }, [dialogRef]);
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/MediaLibrary/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './MediaLibrary';
2 | export * from './MediaLibrary';
3 |
--------------------------------------------------------------------------------
/src/components/MediaList/AlbumList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MediaAlbum from 'types/MediaAlbum';
3 | import MediaSourceLayout from 'types/MediaSourceLayout';
4 | import MediaList, {MediaListProps} from './MediaList';
5 |
6 | const defaultLayout: MediaSourceLayout = {
7 | view: 'card compact',
8 | fields: ['Thumbnail', 'Title', 'Artist', 'Year'],
9 | };
10 |
11 | export default function AlbumList({
12 | className = '',
13 | layout = defaultLayout,
14 | draggable = true,
15 | ...props
16 | }: MediaListProps) {
17 | return (
18 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/MediaList/ArtistList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MediaArtist from 'types/MediaArtist';
3 | import MediaSourceLayout from 'types/MediaSourceLayout';
4 | import MediaList, {MediaListProps} from './MediaList';
5 |
6 | const defaultLayout: MediaSourceLayout = {
7 | view: 'card compact',
8 | fields: ['Thumbnail', 'Title', 'Genre'],
9 | };
10 |
11 | export default function ArtistList({
12 | className = '',
13 | layout = defaultLayout,
14 | ...props
15 | }: MediaListProps) {
16 | return ;
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/MediaList/MediaItemList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MediaItem from 'types/MediaItem';
3 | import MediaSourceLayout from 'types/MediaSourceLayout';
4 | import MediaList, {MediaListProps} from './MediaList';
5 |
6 | const defaultLayout: MediaSourceLayout = {
7 | view: 'details',
8 | fields: ['Artist', 'Title', 'Album', 'Track', 'Duration', 'Genre', 'PlayCount'],
9 | };
10 |
11 | export default function MediaItemList({
12 | className = '',
13 | layout = defaultLayout,
14 | multiple = true,
15 | draggable = true,
16 | ...props
17 | }: MediaListProps) {
18 | return (
19 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/MediaList/MediaListStatusBar.scss:
--------------------------------------------------------------------------------
1 | .media-list-status-bar {
2 | line-height: normal;
3 |
4 | p {
5 | display: flex;
6 | flex-direction: row;
7 | align-items: center;
8 | }
9 |
10 | .progress-ring {
11 | margin-right: 0.25em;
12 | }
13 |
14 | .message.error {
15 | color: inherit;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/MediaList/PlaylistList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MediaPlaylist from 'types/MediaPlaylist';
3 | import MediaSourceLayout from 'types/MediaSourceLayout';
4 | import MediaList, {MediaListProps} from './MediaList';
5 |
6 | const defaultLayout: MediaSourceLayout = {
7 | view: 'card compact',
8 | fields: ['Thumbnail', 'Title', 'TrackCount', 'Owner', 'Progress'],
9 | };
10 |
11 | export default function PlaylistList({
12 | className = '',
13 | layout = defaultLayout,
14 | ...props
15 | }: MediaListProps) {
16 | return ;
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/MediaList/ProgressRing.scss:
--------------------------------------------------------------------------------
1 | .progress-ring {
2 | &.busy {
3 | animation: progress-spin 1s linear infinite;
4 | }
5 | }
6 |
7 | @keyframes progress-spin {
8 | 0% {
9 | transform: rotate(0deg);
10 | }
11 |
12 | 100% {
13 | transform: rotate(1turn);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/MediaList/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './MediaList';
2 | export * from './MediaList';
3 |
--------------------------------------------------------------------------------
/src/components/MediaList/useIsPlaylistPlayable.ts:
--------------------------------------------------------------------------------
1 | import MediaPlaylist from 'types/MediaPlaylist';
2 | import usePager from 'hooks/usePager';
3 |
4 | export default function useIsPlaylistPlayable(playlist?: MediaPlaylist): boolean {
5 | const pager = playlist?.pager || null;
6 | const [{items}] = usePager(pager);
7 | if (pager) {
8 | const pageSize = pager.pageSize;
9 | const playlistSize = playlist!.trackCount ?? pager.maxSize;
10 | const itemCount = items.reduce((total) => (total += 1), 0);
11 | return playlistSize == null ? false : itemCount + 2 * pageSize >= playlistSize;
12 | } else {
13 | return false;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/MediaList/useOnDragStart.ts:
--------------------------------------------------------------------------------
1 | import {useCallback} from 'react';
2 | import MediaObject from 'types/MediaObject';
3 |
4 | export default function useOnDragStart(selectedItems: readonly T[]) {
5 | return useCallback(
6 | (event: React.DragEvent) => {
7 | const spotifyTracks = selectedItems.filter((item) => item.src.startsWith('spotify:track:'));
8 | if (spotifyTracks.length > 0) {
9 | // Allow dragging of Spotify tracks to external Spotify apps.
10 | const trackUris = spotifyTracks.map((item) => item.src);
11 | event.dataTransfer.setData('text/x-spotify-tracks', trackUris.join('\n'));
12 | event.dataTransfer.effectAllowed = 'copyMove';
13 | }
14 | },
15 | [selectedItems]
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/MediaPlayback/MediaPlayback.scss:
--------------------------------------------------------------------------------
1 | .media-playback {
2 | .panel.playback {
3 | bottom: 0;
4 |
5 | @media (display-mode: window-controls-overlay) {
6 | top: max(1.5rem, 32px);
7 | }
8 |
9 | .electron & {
10 | top: env(titlebar-area-height, 1.5rem);
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/MediaPlayback/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './MediaPlayback';
2 | export * from './MediaPlayback';
3 |
--------------------------------------------------------------------------------
/src/components/MediaSources/MediaSourceLabel.scss:
--------------------------------------------------------------------------------
1 | .media-source-label {
2 | display: flex;
3 | flex-direction: row;
4 | align-items: center;
5 |
6 | .icon {
7 | min-width: 1em;
8 | margin-right: 0.25em;
9 |
10 | &-folder,
11 | &-folder-up {
12 | margin-top: -0.125em;
13 | }
14 |
15 | &-pin {
16 | transition: transform 0.5s;
17 | }
18 | }
19 |
20 | &.unpinned {
21 | .icon-pin {
22 | transform: rotate(0.5turn);
23 |
24 | & + .text {
25 | text-decoration: line-through;
26 | }
27 | }
28 | }
29 |
30 | .text {
31 | display: inline-block;
32 | overflow: hidden;
33 | text-overflow: ellipsis;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/MediaSources/ProvidedBy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MediaItem from 'types/MediaItem';
3 | import {isPublicMediaService} from 'services/mediaServices';
4 | import MediaSourceLabel from './MediaSourceLabel';
5 | import useMediaServices from 'hooks/useMediaServices';
6 |
7 | export default function ProvidedBy({item}: {item: MediaItem | null}) {
8 | const services = useMediaServices();
9 | const [serviceId] = item?.src.split(':') || [];
10 | const service = services.find((service) => service.id === serviceId);
11 |
12 | if (item && service && isPublicMediaService(service)) {
13 | return (
14 |
19 | );
20 | } else {
21 | return null;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/MediaSources/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './MediaSources';
2 | export * from './MediaSources';
3 |
--------------------------------------------------------------------------------
/src/components/MiniPlayer/MiniPlayer.scss:
--------------------------------------------------------------------------------
1 | .mini-player {
2 | position: absolute;
3 | inset: 0;
4 | overflow: hidden;
5 |
6 | .app-title {
7 | position: absolute;
8 | left: var(--gutter-width);
9 | top: calc(var(--gutter-width) / 2);
10 | }
11 |
12 | .app-drag-region {
13 | height: 2rem;
14 | }
15 |
16 | .media {
17 | top: calc(var(--gutter-width) / 2 + 2rem);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/MiniPlayer/MiniPlayer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AppDragRegion from 'components/App/AppDragRegion';
3 | import AppTitle from 'components/App/AppTitle';
4 | import Media from 'components/Media';
5 | import './MiniPlayer.scss';
6 |
7 | export default function MiniPlayer() {
8 | return (
9 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/MiniPlayer/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './MiniPlayer';
2 | export * from './MiniPlayer';
3 |
--------------------------------------------------------------------------------
/src/components/Playlist/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './Playlist';
2 | export * from './Playlist';
3 |
--------------------------------------------------------------------------------
/src/components/PopupMenu/PopupMenuItemCheckbox.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Except} from 'type-fest';
3 | import PopupMenuItem, {PopupMenuItemProps} from './PopupMenuItem';
4 |
5 | export interface PopupMenuItemCheckboxProps
6 | extends Except, 'role'> {
7 | checked?: boolean;
8 | }
9 |
10 | export default function PopupMenuItemCheckbox({
11 | className = '',
12 | checked = false,
13 | ...props
14 | }: PopupMenuItemCheckboxProps) {
15 | return (
16 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/PopupMenu/PopupMenuSeparator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function PopupMenuSeparator() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/PopupMenu/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './PopupMenu';
2 | export * from './PopupMenu';
3 | export {default as PopupMenuItem} from './PopupMenuItem';
4 | export * from './PopupMenuItem';
5 | export {default as PopupMenuItemCheckbox} from './PopupMenuItemCheckbox';
6 | export * from './PopupMenuItemCheckbox';
7 | export {default as PopupMenuSeparator} from './PopupMenuSeparator';
8 | export * from './PopupMenuSeparator';
9 | export {default as showPopupMenu} from './showPopupMenu';
10 |
--------------------------------------------------------------------------------
/src/components/Scrollable/FixedHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export interface FixedHeaderProps {
4 | children: React.ReactNode;
5 | }
6 |
7 | export default function FixedHeader({children}: FixedHeaderProps) {
8 | return <>{children}>;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Scrollable/Scrollable.scss:
--------------------------------------------------------------------------------
1 | .scrollable {
2 | --scrollbar-size: calc(1.5em * var(--scrollbar-thickness));
3 |
4 | overflow: hidden;
5 | border-radius: inherit;
6 |
7 | &,
8 | &-content {
9 | position: absolute;
10 | inset: 0;
11 | }
12 |
13 | &-content {
14 | isolation: isolate;
15 | display: flex;
16 | flex-direction: column;
17 | min-width: calc(2 * var(--scrollbar-size));
18 | min-height: calc(2 * var(--scrollbar-size));
19 | }
20 |
21 | &-head {
22 | flex: initial;
23 | }
24 |
25 | &-body {
26 | flex: auto;
27 | overflow: hidden;
28 | }
29 |
30 | &-body-content {
31 | position: relative;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Scrollable/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './Scrollable';
2 | export * from './Scrollable';
3 |
--------------------------------------------------------------------------------
/src/components/SearchBar/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './SearchBar';
2 | export * from './SearchBar';
3 |
--------------------------------------------------------------------------------
/src/components/Settings/AdvancedSettings/AdvancedSettings.scss:
--------------------------------------------------------------------------------
1 | .advanced-settings {
2 | .dialog-button-submit {
3 | display: none;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Settings/AdvancedSettings/AdvancedSettings.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TabList, {TabItem} from 'components/TabList';
3 | import Backup from './Backup';
4 | import Logs from './Logs';
5 | import Troubleshooting from './Troubleshooting';
6 | import './AdvancedSettings.scss';
7 |
8 | const tabs: TabItem[] = [
9 | {
10 | tab: 'Logs',
11 | panel: ,
12 | },
13 | {
14 | tab: 'Backup',
15 | panel: ,
16 | },
17 | {
18 | tab: 'Troubleshooting',
19 | panel: ,
20 | },
21 | ];
22 |
23 | export default function AdvancedSettings() {
24 | return ;
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Settings/AdvancedSettings/Backup.scss:
--------------------------------------------------------------------------------
1 | .backup {
2 | .checkbox-list {
3 | max-height: 8em;
4 |
5 | & + p {
6 | margin-top: 1.25em;
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Settings/AdvancedSettings/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './AdvancedSettings';
2 | export * from './AdvancedSettings';
3 |
--------------------------------------------------------------------------------
/src/components/Settings/AdvancedSettings/useLogs.ts:
--------------------------------------------------------------------------------
1 | import Logger, {Log} from 'utils/Logger';
2 | import useObservable from 'hooks/useObservable';
3 |
4 | export default function useLogs(): readonly Log[] {
5 | return useObservable(Logger.observeLogs, Logger.logs);
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Settings/AppSettings/AppSettings.scss:
--------------------------------------------------------------------------------
1 | .app-settings {
2 | &-general {
3 | fieldset p {
4 | margin: 0;
5 | }
6 | }
7 |
8 | .app-preferences {
9 | kbd {
10 | font: inherit;
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Settings/AppSettings/AppSettings.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TabList, {TabItem} from 'components/TabList';
3 | import AppSettingsGeneral from './AppSettingsGeneral';
4 | import AppPreferences from './AppPreferences';
5 | import './AppSettings.scss';
6 |
7 | const tabs: TabItem[] = [
8 | {
9 | tab: 'General',
10 | panel: ,
11 | },
12 | {
13 | tab: 'Preferences',
14 | panel: ,
15 | },
16 | ];
17 |
18 | export default function AppSettings() {
19 | return ;
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Settings/AppSettings/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './AppSettings';
2 | export * from './AppSettings';
3 |
--------------------------------------------------------------------------------
/src/components/Settings/AppearanceSettings/AppearanceSettings.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TabList, {TabItem} from 'components/TabList';
3 | import AppearanceSettingsGeneral from './AppearanceSettingsGeneral';
4 | import ThemeEditor from './ThemeEditor';
5 | import UserThemes from './UserThemes';
6 |
7 | const tabs: TabItem[] = [
8 | {
9 | tab: 'General',
10 | panel: ,
11 | },
12 | {
13 | tab: 'Theme Editor',
14 | panel: ,
15 | },
16 | {
17 | tab: 'My Themes',
18 | panel: ,
19 | },
20 | ];
21 |
22 | export default function AppearanceSettings() {
23 | return ;
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Settings/AppearanceSettings/AppearanceSettingsGeneral.scss:
--------------------------------------------------------------------------------
1 | .appearance-settings-general {
2 | .table-layout label {
3 | display: inline;
4 | }
5 |
6 | .font-size label {
7 | display: table-cell;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Settings/AppearanceSettings/ThemeEditor/SaveThemeDialog.scss:
--------------------------------------------------------------------------------
1 | .save-theme-dialog {
2 | select {
3 | width: 100%;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Settings/AppearanceSettings/ThemeEditor/ThemeColor.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback} from 'react';
2 | import {Except} from 'type-fest';
3 | import theme, {ThemeColorName} from 'services/theme';
4 |
5 | export interface ThemeColorProps
6 | extends Except, 'onChange'> {
7 | colorName: ThemeColorName;
8 | onChange?: (value: string) => void;
9 | }
10 |
11 | export default function ThemeColor({colorName, onChange, ...props}: ThemeColorProps) {
12 | const handleChange = useCallback(
13 | (event: React.ChangeEvent) => {
14 | const value = event.target.value;
15 | theme[colorName] = value;
16 | onChange?.(value);
17 | },
18 | [colorName, onChange]
19 | );
20 |
21 | return ;
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/Settings/AppearanceSettings/ThemeEditor/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './ThemeEditor';
2 | export * from './ThemeEditor';
3 |
--------------------------------------------------------------------------------
/src/components/Settings/AppearanceSettings/ThemeEditor/saveTheme.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import theme from 'services/theme';
3 | import themeStore from 'services/theme/themeStore';
4 | import {DialogProps, showDialog} from 'components/Dialog';
5 | import SaveThemeDialog from './SaveThemeDialog';
6 |
7 | export default async function saveTheme(suggestedName: string): Promise {
8 | const name = await showDialog(
9 | (props: DialogProps) => ,
10 | true
11 | );
12 | if (name) {
13 | theme.name = name;
14 | theme.userTheme = true;
15 | await themeStore.addUserTheme(theme.current);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/Settings/AppearanceSettings/UserThemes.scss:
--------------------------------------------------------------------------------
1 | .user-themes {
2 | &-buttons {
3 | text-align: right;
4 | display: flex;
5 | flex-direction: row;
6 | }
7 |
8 | &-import {
9 | margin-top: 2em;
10 | }
11 |
12 | &-delete {
13 | margin-left: auto;
14 | }
15 |
16 | input[type="file"] {
17 | position: absolute;
18 | visibility: hidden;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Settings/AppearanceSettings/confirmDeleteTheme.ts:
--------------------------------------------------------------------------------
1 | import {confirm} from 'components/Dialog';
2 |
3 | export default async function confirmDeleteTheme(name: string): Promise {
4 | return confirm({
5 | icon: 'palette',
6 | title: 'My Themes',
7 | message: `Delete theme '${name}'?`,
8 | okLabel: 'Delete',
9 | storageId: 'delete-user-theme',
10 | system: true,
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Settings/AppearanceSettings/confirmOverwriteTheme.ts:
--------------------------------------------------------------------------------
1 | import themeStore from 'services/theme/themeStore';
2 | import {confirm} from 'components/Dialog';
3 |
4 | export default async function confirmOverwriteTheme(name: string): Promise {
5 | if (!themeStore.getUserTheme(name)) {
6 | return true;
7 | }
8 | return confirm({
9 | icon: 'palette',
10 | title: 'My Themes',
11 | message: `Overwrite existing theme '${name}'?`,
12 | okLabel: 'Save',
13 | storageId: 'overwrite-user-theme',
14 | system: true,
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Settings/AppearanceSettings/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './AppearanceSettings';
2 | export * from './AppearanceSettings';
3 |
4 |
--------------------------------------------------------------------------------
/src/components/Settings/AppearanceSettings/useCurrentTheme.ts:
--------------------------------------------------------------------------------
1 | import {useMemo} from 'react';
2 | import {map, merge} from 'rxjs';
3 | import theme, {CurrentTheme} from 'services/theme';
4 | import themeStore from 'services/theme/themeStore';
5 | import useObservable from 'hooks/useObservable';
6 |
7 | export default function useCurrentTheme(): CurrentTheme {
8 | const observeTheme = useMemo(
9 | () => () => {
10 | // Make sure we get a fresh object.
11 | return merge(theme.observe(), themeStore.observeUserThemes()).pipe(
12 | map(() => ({...theme.current}))
13 | );
14 | },
15 | []
16 | );
17 | return useObservable(observeTheme, theme.current);
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Settings/AppearanceSettings/useDefaultThemes.ts:
--------------------------------------------------------------------------------
1 | import {useMemo} from 'react';
2 | import themeStore from 'services/theme/themeStore';
3 |
4 | export default function useDefaultThemes() {
5 | return useMemo(() => themeStore.getDefaultThemes(), []);
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Settings/AppearanceSettings/useUserThemes.ts:
--------------------------------------------------------------------------------
1 | import {useMemo} from 'react';
2 | import themeStore from 'services/theme/themeStore';
3 | import useObservable from 'hooks/useObservable';
4 |
5 | export default function useUserThemes() {
6 | const observeUserThemes = useMemo(() => () => themeStore.observeUserThemes(), []);
7 | return useObservable(observeUserThemes, themeStore.getUserThemes());
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/Settings/MediaLibrarySettings/AudioSettings.scss:
--------------------------------------------------------------------------------
1 | .audio-settings {
2 | select,
3 | input {
4 | max-width: 6em;
5 | }
6 |
7 | .warning {
8 | margin-top: 1.5em;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/Settings/MediaLibrarySettings/CredentialsDialog.scss:
--------------------------------------------------------------------------------
1 | .credentials-dialog {
2 | width: 32em;
3 | height: 34em;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Settings/MediaLibrarySettings/DisconnectButton.scss:
--------------------------------------------------------------------------------
1 | .disconnect-button {
2 | width: 100%;
3 |
4 | &:disabled {
5 | background-image: none;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/Settings/MediaLibrarySettings/MediaServiceList.scss:
--------------------------------------------------------------------------------
1 | .media-service-list {
2 | & > li:not(.no-icon) {
3 | align-items: baseline;
4 | }
5 |
6 | & + & {
7 | margin-top: 1em;
8 | border-top: 1px solid;
9 | padding-top: 1em;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Settings/MediaLibrarySettings/MediaServiceSettingsGeneral.scss:
--------------------------------------------------------------------------------
1 | .media-service-settings-general {
2 | .media-source-label {
3 | display: inline-flex;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Settings/MediaLibrarySettings/PersonalMediaServerInfo.scss:
--------------------------------------------------------------------------------
1 | .personal-media-server-info {
2 | .table-layout p {
3 | line-height: normal;
4 | }
5 |
6 | input {
7 | border: none;
8 | background: none;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/Settings/MediaLibrarySettings/PersonalMediaServerSettings.scss:
--------------------------------------------------------------------------------
1 | .personal-media-server-settings {
2 | .external-link-text {
3 | text-decoration: underline;
4 | }
5 |
6 | select {
7 | min-width: 8em;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Settings/MediaLibrarySettings/PinnedSettings.scss:
--------------------------------------------------------------------------------
1 | .pinned-settings {
2 | &-buttons {
3 | text-align: right;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Settings/MediaLibrarySettings/confirmDisconnectServices.scss:
--------------------------------------------------------------------------------
1 | .confirm-disconnect-services {
2 | text-align: left;
3 |
4 | ul {
5 | margin: 1em 2em;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/Settings/MediaLibrarySettings/usePinsForService.ts:
--------------------------------------------------------------------------------
1 | import {useMemo} from 'react';
2 | import MediaService from 'types/MediaService';
3 | import Pin from 'types/Pin';
4 | import pinStore from 'services/pins/pinStore';
5 | import useObservable from 'hooks/useObservable';
6 |
7 | export default function usePinsForService(service: MediaService): readonly Pin[] {
8 | const observePinsForService = useMemo(
9 | () => () => pinStore.observePinsForService(service.id),
10 | [service]
11 | );
12 | return useObservable(observePinsForService, pinStore.getPinsForService(service.id));
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Settings/MediaLibrarySettings/useServerInfo.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import {defer} from 'rxjs';
3 | import PersonalMediaService from 'types/PersonalMediaService';
4 | import useIsLoggedIn from 'hooks/useIsLoggedIn';
5 |
6 | export default function useServerInfo(service: PersonalMediaService) {
7 | const isLoggedIn = useIsLoggedIn(service);
8 | const [serverInfo, setServerInfo] = useState>({});
9 |
10 | useEffect(() => {
11 | if (isLoggedIn && service.getServerInfo) {
12 | const subscription = defer(() => service.getServerInfo!()).subscribe(setServerInfo);
13 | return () => subscription.unsubscribe();
14 | }
15 | }, [service, isLoggedIn]);
16 |
17 | return serverInfo;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Settings/VisualizerSettings/AmpshaderSettings.tsx:
--------------------------------------------------------------------------------
1 | import React, {useId} from 'react';
2 | import visualizerSettings from 'services/visualizer/visualizerSettings';
3 |
4 | export default function AmpshaderSettings() {
5 | const id = useId();
6 |
7 | return (
8 |
9 | Options
10 |
11 | (visualizerSettings.ampshaderTransparency = e.target.checked)}
16 | />
17 | Use transparency for light themes
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Settings/VisualizerSettings/ButterchurnSettings.scss:
--------------------------------------------------------------------------------
1 | .butterchurn-settings {
2 | .table-layout {
3 | select,
4 | input {
5 | max-width: max-content;
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/src/components/Settings/VisualizerSettings/VisualizerFavorites.scss:
--------------------------------------------------------------------------------
1 | .visualizer-favorites {
2 | .list-view {
3 | width: 100%;
4 | height: 20em;
5 | margin: 0.5em 0;
6 | }
7 |
8 | &-buttons {
9 | display: flex;
10 | flex-direction: row;
11 | align-items: baseline;
12 | }
13 |
14 | &-delete {
15 | margin-top: 0.5em;
16 | margin-left: auto;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Settings/VisualizerSettings/VisualizerSettings.scss:
--------------------------------------------------------------------------------
1 | .visualizer-settings {
2 | &-dialog {
3 | width: 33.5em;
4 | height: 36.5em;
5 | }
6 |
7 | select + button {
8 | margin-left: 1em;
9 | }
10 |
11 | .ambient-video-settings {
12 | input[type="url"] {
13 | width: 100%;
14 | }
15 |
16 | fieldset + p {
17 | margin-top: 1.5em;
18 | }
19 | }
20 |
21 | &-general fieldset {
22 | margin-top: 1.5em;
23 | }
24 |
25 | .use-provider {
26 | padding-top: 0;
27 | padding-bottom: 0;
28 |
29 | &.in-use {
30 | opacity: 1;
31 | color: var(--background-color);
32 | background: var(--text-color);
33 | }
34 | }
35 |
36 | .compatibility {
37 | margin-top: 2em;
38 | font-style: italic;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/Settings/VisualizerSettings/VisualizerSettings.tsx:
--------------------------------------------------------------------------------
1 | import React, {useMemo} from 'react';
2 | import {t} from 'services/i18n';
3 | import TabList, {TabItem} from 'components/TabList';
4 | import VisualizerSettingsGeneral from './VisualizerSettingsGeneral';
5 | import VisualizerFavorites from './VisualizerFavorites';
6 | import './VisualizerSettings.scss';
7 |
8 | export default function VisualizerSettings() {
9 | const tabs: TabItem[] = useMemo(
10 | () => [
11 | {
12 | tab: 'General',
13 | panel: ,
14 | },
15 | {
16 | tab: t('Favorites'),
17 | panel: ,
18 | },
19 | ],
20 | []
21 | );
22 |
23 | return ;
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Settings/VisualizerSettings/VisualizerSettingsDialog.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Dialog, {DialogProps} from 'components/Dialog';
3 | import VisualizerSettings from './VisualizerSettings';
4 |
5 | export default function VisualizerSettingsDialog(props: DialogProps) {
6 | return (
7 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Settings/VisualizerSettings/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './VisualizerSettings';
2 | export * from './VisualizerSettings';
3 | export {default as VisualizerSettingsDialog} from './VisualizerSettingsDialog';
4 |
--------------------------------------------------------------------------------
/src/components/Settings/index.ts:
--------------------------------------------------------------------------------
1 | export {default as SettingsDialog} from './SettingsDialog';
2 | export * from './SettingsDialog';
3 | export * from './AppearanceSettings';
4 | export * from './VisualizerSettings';
5 |
--------------------------------------------------------------------------------
/src/components/Splitter/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './Splitter';
2 | export * from './Splitter';
3 |
--------------------------------------------------------------------------------
/src/components/Splitter/layoutSettings.ts:
--------------------------------------------------------------------------------
1 | import {LiteStorage} from 'utils';
2 |
3 | const storage = new LiteStorage('layout/2');
4 |
5 | export default {
6 | get(id: string, defaultValue = 0): number {
7 | return storage.getNumber(id, defaultValue);
8 | },
9 |
10 | set(id: string, value: number): void {
11 | storage.setNumber(id, value);
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/StarRating/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './StarRating';
2 | export * from './StarRating';
3 |
--------------------------------------------------------------------------------
/src/components/StartupWizard/StartupWizard.scss:
--------------------------------------------------------------------------------
1 | .startup-wizard {
2 | width: 24em;
3 | height: 34em;
4 |
5 | h3 {
6 | font-size: 1.375em;
7 | margin-bottom: 0.75em;
8 | }
9 |
10 | form {
11 | width: 100%;
12 | }
13 |
14 | .personal-media-services .media-service-list {
15 | font-size: 1.125em;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/StartupWizard/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './StartupWizard';
2 | export * from './StartupWizard';
3 |
--------------------------------------------------------------------------------
/src/components/StatusBar/StatusBar.scss:
--------------------------------------------------------------------------------
1 | .status-bar {
2 | padding: var(--gutter-width) 0;
3 | font-size: 0.75em;
4 | white-space: nowrap;
5 | overflow: hidden;
6 | text-overflow: ellipsis;
7 | user-select: none;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/StatusBar/StatusBar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './StatusBar.scss';
3 |
4 | export interface StatusBarProps {
5 | className?: string;
6 | children: React.ReactNode;
7 | }
8 |
9 | export default function StatusBar({className = '', children}: StatusBarProps) {
10 | return ;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/StatusBar/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './StatusBar';
2 | export * from './StatusBar';
3 |
--------------------------------------------------------------------------------
/src/components/SunClock/SunClock.scss:
--------------------------------------------------------------------------------
1 | .sun-clock {
2 | --sun-color: var(--text-color-h), var(--text-color-s), var(--text-color-l);
3 | --am-alpha: 0%;
4 | --pm-alpha: 0%;
5 | --am-color: hsla(var(--sun-color), var(--am-alpha));
6 | --pm-color: hsla(var(--sun-color), var(--pm-alpha));
7 |
8 | display: inline-block;
9 | width: 1em;
10 | height: 1em;
11 | margin-right: 0.1875em;
12 | background-image: linear-gradient(180deg, var(--am-color) 50%, var(--pm-color) 50%);
13 | border-radius: 50%;
14 | border: 0.125em solid;
15 | aspect-ratio: 1;
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/SunClock/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './SunClock';
2 | export * from './SunClock';
3 |
--------------------------------------------------------------------------------
/src/components/TabList/TabPanel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {TabItem} from './TabList';
3 |
4 | export interface TabPanelProps {
5 | id: string;
6 | item: TabItem;
7 | index: number;
8 | hidden: boolean;
9 | }
10 |
11 | export default function TabPanel({id, item, index, hidden}: TabPanelProps) {
12 | return (
13 |
20 | {item.panel}
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/TabList/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './TabList';
2 | export * from './TabList';
3 |
--------------------------------------------------------------------------------
/src/components/TextBox/TextBox.scss:
--------------------------------------------------------------------------------
1 | .text-box {
2 | line-height: 1.25;
3 |
4 | p {
5 | margin: 0 0 1em 0;
6 | }
7 |
8 | .scrollable.overflow-y &-content {
9 | padding-right: 1ex;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/TextBox/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './TextBox';
2 | export * from './TextBox';
3 |
--------------------------------------------------------------------------------
/src/components/Time/Time.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Except} from 'type-fest';
3 | import {formatTime} from 'utils';
4 |
5 | export interface TimeProps extends Except, 'children'> {
6 | time: number;
7 | }
8 |
9 | export default function Time({time, ...props}: TimeProps) {
10 | return {formatTime(time)} ;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Time/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './Time';
2 | export * from './Time';
3 |
--------------------------------------------------------------------------------
/src/components/TreeView/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './TreeView';
2 | export * from './TreeView';
3 |
--------------------------------------------------------------------------------
/src/hooks/useBaseFontSize.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import theme from 'services/theme';
3 |
4 | export default function useBaseFontSize(): number {
5 | const [fontSize, setFontSize] = useState(theme.fontSize);
6 |
7 | useEffect(() => {
8 | const subscription = theme.observeFontSize().subscribe(setFontSize);
9 | return () => subscription.unsubscribe();
10 | }, []);
11 |
12 | return fontSize;
13 | }
14 |
--------------------------------------------------------------------------------
/src/hooks/useCurrentTime.ts:
--------------------------------------------------------------------------------
1 | import playback, {observeCurrentTime} from 'services/mediaPlayback/playback';
2 | import useObservable from './useObservable';
3 |
4 | export default function useCurrentTime(): number {
5 | return useObservable(observeCurrentTime, playback.currentTime);
6 | }
7 |
--------------------------------------------------------------------------------
/src/hooks/useCurrentTrack.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import PlaylistItem from 'types/PlaylistItem';
3 | import playback, {observeCurrentItem} from 'services/mediaPlayback/playback';
4 |
5 | export default function useCurrentTrack(): PlaylistItem | null {
6 | const [value, setValue] = useState(playback.currentItem);
7 |
8 | useEffect(() => {
9 | const subscription = observeCurrentItem().subscribe(setValue);
10 | return () => subscription.unsubscribe();
11 | }, []);
12 |
13 | return value;
14 | }
15 |
--------------------------------------------------------------------------------
/src/hooks/useCurrentVisualizer.ts:
--------------------------------------------------------------------------------
1 | import Visualizer from 'types/Visualizer';
2 | import {getCurrentVisualizer, observeCurrentVisualizer} from 'services/visualizer';
3 | import useObservable from './useObservable';
4 |
5 | export default function useCurrentVisualizer(): Visualizer | null {
6 | return useObservable(observeCurrentVisualizer, getCurrentVisualizer());
7 | }
8 |
--------------------------------------------------------------------------------
/src/hooks/useCurrentlyPlaying.ts:
--------------------------------------------------------------------------------
1 | import PlaylistItem from 'types/PlaylistItem';
2 | import {getCurrentItem, observeCurrentItem} from 'services/playlist';
3 | import useObservable from './useObservable';
4 |
5 | export default function useCurrentlyPlaying(): PlaylistItem | null {
6 | return useObservable(observeCurrentItem, getCurrentItem());
7 | }
8 |
--------------------------------------------------------------------------------
/src/hooks/useDebouncedValue.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import {debounceTime} from 'rxjs';
3 | import useSubject from './useSubject';
4 |
5 | export default function useDebouncedValue(initialValue: T, dueTime: number) {
6 | const [debouncedValue, setDebouncedValue] = useState(initialValue);
7 | const [value$, nextValue] = useSubject();
8 |
9 | useEffect(() => {
10 | const subscription = value$.pipe(debounceTime(dueTime)).subscribe(setDebouncedValue);
11 | return () => subscription.unsubscribe();
12 | }, [value$, dueTime]);
13 |
14 | return [debouncedValue, nextValue] as const;
15 | }
16 |
--------------------------------------------------------------------------------
/src/hooks/useDuration.ts:
--------------------------------------------------------------------------------
1 | import playback, {observeDuration} from 'services/mediaPlayback/playback';
2 | import useObservable from './useObservable';
3 |
4 | export default function useDuration(): number {
5 | return useObservable(observeDuration, playback.duration);
6 | }
7 |
--------------------------------------------------------------------------------
/src/hooks/useFirstValue.ts:
--------------------------------------------------------------------------------
1 | import {useRef} from 'react';
2 |
3 | export default function useFirstValue(value: T) {
4 | const valueRef = useRef(value);
5 | if (valueRef.current == null && value != null) {
6 | valueRef.current = value;
7 | }
8 | return valueRef.current;
9 | }
10 |
--------------------------------------------------------------------------------
/src/hooks/useIsLoggedIn.ts:
--------------------------------------------------------------------------------
1 | import MediaService from 'types/MediaService';
2 | import useObservable from './useObservable';
3 |
4 | export default function useIsLoggedIn(service: MediaService): boolean {
5 | return useObservable(service.observeIsLoggedIn, service.isLoggedIn());
6 | }
7 |
--------------------------------------------------------------------------------
/src/hooks/useIsOnLine.ts:
--------------------------------------------------------------------------------
1 | import {observeIsOnLine, isOnLine} from 'services/online';
2 | import useObservable from './useObservable';
3 |
4 | export default function useIsOnLine(): boolean {
5 | return useObservable(observeIsOnLine, isOnLine());
6 | }
7 |
--------------------------------------------------------------------------------
/src/hooks/useIsPlaying.ts:
--------------------------------------------------------------------------------
1 | import usePlaybackState from './usePlaybackState';
2 |
3 | export default function useIsPlaying(): boolean {
4 | const {paused, currentTime} = usePlaybackState();
5 | return !paused && currentTime > 0;
6 | }
7 |
--------------------------------------------------------------------------------
/src/hooks/useMediaServices.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import {defer} from 'rxjs';
3 | import MediaService from 'types/MediaService';
4 | import {getServices, loadMediaServices} from 'services/mediaServices';
5 |
6 | export default function useMediaServices() {
7 | const [services, setServices] = useState(() => getServices());
8 |
9 | useEffect(() => {
10 | const subscription = defer(() => loadMediaServices()).subscribe(setServices);
11 | return () => subscription.unsubscribe();
12 | }, []);
13 |
14 | return services;
15 | }
16 |
--------------------------------------------------------------------------------
/src/hooks/useMiniPlayerActive.ts:
--------------------------------------------------------------------------------
1 | import miniPlayer from 'services/mediaPlayback/miniPlayer';
2 | import useObservable from './useObservable';
3 |
4 | export default function useMiniPlayerActive(): boolean {
5 | return useObservable(miniPlayer.observeActive, miniPlayer.active);
6 | }
7 |
--------------------------------------------------------------------------------
/src/hooks/useObservable.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import type {Observable} from 'rxjs';
3 |
4 | export default function useObservable(observe: () => Observable, initialValue: T): T {
5 | const [value, setValue] = useState(initialValue);
6 |
7 | useEffect(() => {
8 | const subscription = observe().subscribe(setValue);
9 | return () => subscription.unsubscribe();
10 | }, [observe]);
11 |
12 | return value;
13 | }
14 |
--------------------------------------------------------------------------------
/src/hooks/usePaused.ts:
--------------------------------------------------------------------------------
1 | import playback, {observePaused} from 'services/mediaPlayback/playback';
2 | import useObservable from './useObservable';
3 |
4 | export default function usePaused(): boolean {
5 | return useObservable(observePaused, playback.paused);
6 | }
7 |
--------------------------------------------------------------------------------
/src/hooks/usePlaybackState.ts:
--------------------------------------------------------------------------------
1 | import PlaybackState from 'types/PlaybackState';
2 | import {getPlaybackState, observePlaybackState} from 'services/mediaPlayback/playback';
3 | import useObservable from './useObservable';
4 |
5 | export default function usePlaybackState(): PlaybackState {
6 | return useObservable(observePlaybackState, getPlaybackState());
7 | }
8 |
--------------------------------------------------------------------------------
/src/hooks/usePreferences.ts:
--------------------------------------------------------------------------------
1 | import preferences, {observePreferences} from 'services/preferences';
2 | import useObservable from './useObservable';
3 |
4 | export default function usePreferences() {
5 | return useObservable(observePreferences, {...preferences});
6 | }
7 |
--------------------------------------------------------------------------------
/src/hooks/usePrevious.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useRef} from 'react';
2 |
3 | export default function usePrevious(value: T) {
4 | const ref = useRef(undefined);
5 |
6 | useEffect(() => {
7 | ref.current = value;
8 | }, [value]);
9 |
10 | return ref.current;
11 | }
12 |
--------------------------------------------------------------------------------
/src/hooks/useSearch.ts:
--------------------------------------------------------------------------------
1 | import {useMemo} from 'react';
2 | import MediaObject from 'types/MediaObject';
3 | import MediaSearchParams from 'types/MediaSearchParams';
4 | import MediaSource from 'types/MediaSource';
5 | import Pager from 'types/Pager';
6 | import useSource from './useSource';
7 |
8 | export default function useSearch(
9 | source: MediaSource | null,
10 | q: MediaSearchParams['q'] = '',
11 | sortBy?: MediaSearchParams['sortBy'],
12 | sortOrder?: MediaSearchParams['sortOrder']
13 | ): Pager | null {
14 | const params: MediaSearchParams = useMemo(
15 | () => ({q, sortBy, sortOrder}),
16 | [q, sortBy, sortOrder]
17 | );
18 | const pager = useSource(source, params);
19 | return pager;
20 | }
21 |
--------------------------------------------------------------------------------
/src/hooks/useSorting.ts:
--------------------------------------------------------------------------------
1 | import {useMemo} from 'react';
2 | import MediaObject from 'types/MediaObject';
3 | import MediaSource from 'types/MediaSource';
4 | import {getSourceSorting, observeSourceSorting} from 'services/mediaServices/servicesSettings';
5 | import useObservable from './useObservable';
6 |
7 | export default function useSorting(source: MediaSource) {
8 | const observeSorting = useMemo(() => () => observeSourceSorting(source), [source]);
9 | const {sortBy, sortOrder} = useObservable(observeSorting, getSourceSorting(source));
10 | return {sortBy, sortOrder};
11 | }
12 |
--------------------------------------------------------------------------------
/src/hooks/useSubject.ts:
--------------------------------------------------------------------------------
1 | import {useCallback, useRef} from 'react';
2 | import type {Observable} from 'rxjs';
3 | import {Subject} from 'rxjs';
4 |
5 | export default function useSubject(): [Observable, (next: T) => void] {
6 | const ref = useRef | null>(null);
7 |
8 | if (ref.current === null) {
9 | ref.current = new Subject();
10 | }
11 |
12 | const next = useCallback((value: T) => {
13 | ref.current!.next(value);
14 | }, []);
15 |
16 | return [ref.current, next];
17 | }
18 |
--------------------------------------------------------------------------------
/src/hooks/useThrottledValue.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import {throttleTime, ThrottleConfig} from 'rxjs';
3 | import useSubject from './useSubject';
4 |
5 | export default function useThrottledValue(
6 | initialValue: T,
7 | dueTime: number,
8 | {leading = true, trailing = false}: ThrottleConfig = {}
9 | ) {
10 | const [throttledValue, setThrottledValue] = useState(initialValue);
11 | const [value$, nextValue] = useSubject();
12 |
13 | useEffect(() => {
14 | const subscription = value$
15 | .pipe(throttleTime(dueTime, undefined, {leading, trailing}))
16 | .subscribe(setThrottledValue);
17 | return () => subscription.unsubscribe();
18 | }, [value$, dueTime, leading, trailing]);
19 |
20 | return [throttledValue, nextValue] as const;
21 | }
22 |
--------------------------------------------------------------------------------
/src/hooks/useVisualizerProviders.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import {defer} from 'rxjs';
3 | import VisualizerProvider from 'types/VisualizerProvider';
4 | import {getVisualizerProviders, loadVisualizers} from 'services/visualizer/visualizerProviders';
5 |
6 | export default function useVisualizerProviders() {
7 | const [providers, setProviders] = useState(() =>
8 | getVisualizerProviders()
9 | );
10 |
11 | useEffect(() => {
12 | const subscription = defer(() => loadVisualizers()).subscribe(setProviders);
13 | return () => subscription.unsubscribe();
14 | }, []);
15 |
16 | return providers;
17 | }
18 |
--------------------------------------------------------------------------------
/src/hooks/useVisualizerSettings.ts:
--------------------------------------------------------------------------------
1 | import visualizerSettings, {
2 | observeVisualizerSettings,
3 | } from 'services/visualizer/visualizerSettings';
4 | import useObservable from './useObservable';
5 |
6 | export default function useVisualizerSettings() {
7 | return useObservable(observeVisualizerSettings, {...visualizerSettings});
8 | }
9 |
--------------------------------------------------------------------------------
/src/hooks/useYouTubeVideoInfo.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import {defer} from 'rxjs';
3 | import MediaItem from 'types/MediaItem';
4 | import youtubeApi from 'services/youtube/youtubeApi';
5 |
6 | export default function useYouTubeVideoInfo(src: string) {
7 | const [service, , videoId] = src.split(':');
8 | const [videoInfo, setVideoInfo] = useState(null);
9 |
10 | useEffect(() => {
11 | setVideoInfo(null);
12 | if (service === 'youtube' && videoId) {
13 | const subscription = defer(() => youtubeApi.getMediaItem(videoId)).subscribe(
14 | setVideoInfo
15 | );
16 | return () => subscription.unsubscribe();
17 | }
18 | }, [service, videoId]);
19 |
20 | return videoInfo;
21 | }
22 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import 'styles/index.scss';
2 | import React, {StrictMode} from 'react';
3 | import {createRoot} from 'react-dom/client';
4 | import {ErrorBoundary} from 'react-error-boundary';
5 | import {Logger} from 'utils';
6 | import {createErrorReport} from 'services/reporting';
7 | import App from 'components/App';
8 | import BSOD from 'components/Errors/BSOD';
9 | import './registerServiceWorker';
10 |
11 | Logger.createErrorReport = createErrorReport;
12 |
13 | const uncaught = new Logger('uncaught');
14 |
15 | window.onerror = __dev__ ? uncaught.error : uncaught.warn;
16 |
17 | createRoot(document.getElementById('app')!).render(
18 |
19 |
20 |
21 |
22 |
23 | );
24 |
--------------------------------------------------------------------------------
/src/services/ampcastElectron.ts:
--------------------------------------------------------------------------------
1 | import AmpcastElectron from 'types/AmpcastElectron';
2 | import {browser} from 'utils';
3 |
4 | const ampcastElectron: AmpcastElectron | undefined = browser.isElectron
5 | ? (globalThis as any)['ampcastElectron']
6 | : undefined;
7 |
8 | export default ampcastElectron;
9 |
--------------------------------------------------------------------------------
/src/services/apple/AppleBitrate.ts:
--------------------------------------------------------------------------------
1 | const enum AppleBitrate {
2 | Standard = 64,
3 | High = 256,
4 | }
5 |
6 | export default AppleBitrate;
7 |
--------------------------------------------------------------------------------
/src/services/apple/bootstrap.ts:
--------------------------------------------------------------------------------
1 | import mediaPlayer from 'services/mediaPlayback/mediaPlayer';
2 | import musicKitPlayer from './musicKitPlayer';
3 |
4 | mediaPlayer.registerPlayer(musicKitPlayer, (item) => !!item?.src.startsWith('apple:'));
5 |
--------------------------------------------------------------------------------
/src/services/apple/components/useCredentials.ts:
--------------------------------------------------------------------------------
1 | import useObservable from 'hooks/useObservable';
2 | import appleSettings, {AppleCredentials} from '../appleSettings';
3 |
4 | export default function useCredentials(): AppleCredentials {
5 | return useObservable(appleSettings.observeCredentials, appleSettings.getCredentials());
6 | }
7 |
--------------------------------------------------------------------------------
/src/services/apple/components/useMusicKit.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import {defer} from 'rxjs';
3 | import {getMusicKitInstance} from '../appleAuth';
4 |
5 | export default function useMusicKit(devToken: string) {
6 | const [musicKit, setMusicKit] = useState(null);
7 | const [error, setError] = useState(null);
8 |
9 | useEffect(() => {
10 | if (devToken) {
11 | const subscription = defer(() => getMusicKitInstance()).subscribe({
12 | next: setMusicKit,
13 | error: setError,
14 | });
15 | return () => subscription.unsubscribe();
16 | }
17 | }, [devToken]);
18 |
19 | return {musicKit, error};
20 | }
21 |
--------------------------------------------------------------------------------
/src/services/apple/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './apple';
2 | export * from './apple';
3 |
--------------------------------------------------------------------------------
/src/services/audio/OmniAudioContext.ts:
--------------------------------------------------------------------------------
1 | import OmniAnalyserNode from './OmniAnalyserNode';
2 |
3 | export default class OmniAudioContext extends AudioContext {
4 | createAnalyser(): AnalyserNode {
5 | return new OmniAnalyserNode(this);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/services/audio/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './audio';
2 | export * from './audio';
3 |
--------------------------------------------------------------------------------
/src/services/constants.ts:
--------------------------------------------------------------------------------
1 | export const MAX_DURATION = 24 * 60 * 60 - 1;
2 | export const githubRepoUrl = 'https://github.com/rekkyrosso/ampcast';
3 | export const downloadUrl = `${githubRepoUrl}/releases`;
4 | export const dockerUrl = `${githubRepoUrl}/pkgs/container/ampcast`;
5 | export const supportUrl = 'https://www.reddit.com/r/ampcast';
6 |
--------------------------------------------------------------------------------
/src/services/emby/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './emby';
2 | export * from './emby';
3 |
--------------------------------------------------------------------------------
/src/services/i18n.ts:
--------------------------------------------------------------------------------
1 | const en_us = /^en(-us)?$/i.test(navigator.language);
2 |
3 | export function t(string: string): string {
4 | // Let's keep this simple for now.
5 | if (string && !en_us) {
6 | return string.replaceAll(/(F)avorite(s?)/gi, '$1avourite$2');
7 | }
8 | return string;
9 | }
10 |
--------------------------------------------------------------------------------
/src/services/jellyfin/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './jellyfin';
2 | export * from './jellyfin';
3 |
--------------------------------------------------------------------------------
/src/services/jellyfin/jellyfinSettings.ts:
--------------------------------------------------------------------------------
1 | import {EmbySettings} from 'services/emby/embySettings';
2 |
3 | const jellyfinSettings = new EmbySettings('jellyfin');
4 |
5 | export default jellyfinSettings;
6 |
--------------------------------------------------------------------------------
/src/services/lastfm/components/LastFmHistoryBrowser.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MediaItem from 'types/MediaItem';
3 | import MediaService from 'types/MediaService';
4 | import MediaSource from 'types/MediaSource';
5 | import HistoryBrowser from 'components/MediaBrowser/HistoryBrowser';
6 | import useHistoryStart from './useHistoryStart';
7 |
8 | export interface LastFmHistoryBrowserProps {
9 | service: MediaService;
10 | source: MediaSource;
11 | }
12 |
13 | export default function LastFmHistoryBrowser({
14 | service: lastfm,
15 | source: history,
16 | }: LastFmHistoryBrowserProps) {
17 | const minDate = useHistoryStart();
18 |
19 | return ;
20 | }
21 |
--------------------------------------------------------------------------------
/src/services/lastfm/components/useCredentials.ts:
--------------------------------------------------------------------------------
1 | import useObservable from 'hooks/useObservable';
2 | import lastfmSettings, {LastFmCredentials} from '../lastfmSettings';
3 |
4 | export default function useCredentials(): LastFmCredentials {
5 | return useObservable(lastfmSettings.observeCredentials, lastfmSettings.getCredentials());
6 | }
7 |
--------------------------------------------------------------------------------
/src/services/lastfm/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './lastfm';
2 | export * from './lastfm';
3 |
--------------------------------------------------------------------------------
/src/services/listenbrainz/components/ListenBrainzHistoryBrowser.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MediaItem from 'types/MediaItem';
3 | import MediaService from 'types/MediaService';
4 | import MediaSource from 'types/MediaSource';
5 | import HistoryBrowser from 'components/MediaBrowser/HistoryBrowser';
6 | import useHistoryStart from './useHistoryStart';
7 |
8 | export interface ListenBrainzHistoryBrowserProps {
9 | service: MediaService;
10 | source: MediaSource;
11 | }
12 |
13 | export default function ListenBrainzHistoryBrowser({
14 | service: listenbrainz,
15 | source: history,
16 | }: ListenBrainzHistoryBrowserProps) {
17 | const minDate = useHistoryStart();
18 |
19 | return ;
20 | }
21 |
--------------------------------------------------------------------------------
/src/services/listenbrainz/components/ListenBrainzLoginDialog.scss:
--------------------------------------------------------------------------------
1 | .listenbrainz-login-dialog {
2 | .listenbrainz-link {
3 | text-align: center;
4 | margin-bottom: 1em;
5 | font-size: 0.875em;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/services/listenbrainz/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './listenbrainz';
2 | export * from './listenbrainz';
3 |
--------------------------------------------------------------------------------
/src/services/listenbrainz/listenbrainzSettings.ts:
--------------------------------------------------------------------------------
1 | import {LiteStorage} from 'utils';
2 |
3 | const storage = new LiteStorage('listenbrainz');
4 |
5 | export default {
6 | get token(): string {
7 | return storage.getString('token');
8 | },
9 |
10 | set token(token: string) {
11 | storage.setString('token', token);
12 | },
13 |
14 | get userId(): string {
15 | return storage.getString('userId');
16 | },
17 |
18 | set userId(userId: string) {
19 | storage.setString('userId', userId);
20 | },
21 |
22 | get firstScrobbledAt(): string {
23 | return storage.getString('firstScrobbledAt');
24 | },
25 |
26 | set firstScrobbledAt(time: string) {
27 | storage.setString('firstScrobbledAt', time);
28 | },
29 |
30 | clear(): void {
31 | storage.clear();
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/src/services/lookup/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './lookup';
2 | export * from './lookup';
3 | export * from './lookupEvents';
4 |
--------------------------------------------------------------------------------
/src/services/lookup/lookupSettings.ts:
--------------------------------------------------------------------------------
1 | import {LiteStorage} from 'utils';
2 |
3 | const storage = new LiteStorage('lookup');
4 |
5 | export default {
6 | get preferPersonalMedia(): boolean {
7 | return storage.getBoolean('preferPersonalMedia');
8 | },
9 |
10 | set preferPersonalMedia(preferPersonalMedia: boolean) {
11 | storage.setBoolean('preferPersonalMedia', preferPersonalMedia);
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/src/services/mediaPlayback/defaultPlaybackState.ts:
--------------------------------------------------------------------------------
1 | import PlaybackState from 'types/PlaybackState';
2 | import {isMiniPlayer} from 'utils';
3 |
4 | const defaultPlaybackState: PlaybackState = {
5 | currentItem: null,
6 | currentTime: 0,
7 | startedAt: 0,
8 | endedAt: 0,
9 | duration: 0,
10 | paused: true,
11 | playbackId: '',
12 | miniPlayer: isMiniPlayer,
13 | };
14 |
15 | export default defaultPlaybackState;
16 |
--------------------------------------------------------------------------------
/src/services/mediaPlayback/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './mediaPlayback';
2 | export * from './mediaPlayback';
3 |
--------------------------------------------------------------------------------
/src/services/mediaPlayback/mediaPlaybackSettings.ts:
--------------------------------------------------------------------------------
1 | import {LiteStorage} from 'utils';
2 |
3 | const storage = new LiteStorage('mediaPlayback');
4 | const session = new LiteStorage('mediaPlayback', 'session');
5 |
6 | const mediaPlaybackSettings = {
7 | get loop(): boolean {
8 | return session.getBoolean('loop');
9 | },
10 |
11 | set loop(loop: boolean) {
12 | session.setBoolean('loop', loop);
13 | },
14 |
15 | get muted(): boolean {
16 | return session.getBoolean('muted');
17 | },
18 |
19 | set muted(muted: boolean) {
20 | session.setBoolean('muted', muted);
21 | },
22 |
23 | get volume(): number {
24 | return storage.getNumber('volume', 0.7);
25 | },
26 |
27 | set volume(volume: number) {
28 | storage.setNumber('volume', volume);
29 | },
30 | };
31 |
32 | export default mediaPlaybackSettings;
33 |
--------------------------------------------------------------------------------
/src/services/mediaPlayback/players/observeNearEnd.ts:
--------------------------------------------------------------------------------
1 | import type {Observable} from 'rxjs';
2 | import {distinctUntilChanged, map, withLatestFrom} from 'rxjs';
3 | import Player from 'types/Player';
4 |
5 | export default function observeNearEnd(
6 | player: Player,
7 | secondsBeforeEnd: number
8 | ): Observable {
9 | return player.observeCurrentTime().pipe(
10 | withLatestFrom(player.observeDuration()),
11 | map(([currentTime, duration]) => duration - currentTime <= secondsBeforeEnd),
12 | distinctUntilChanged()
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/services/mediaPlayback/scrobbler.ts:
--------------------------------------------------------------------------------
1 | import {filter, mergeMap, tap} from 'rxjs';
2 | import {isMiniPlayer, Logger} from 'utils';
3 | import {addListen} from 'services/localdb/listens';
4 | import {observeMediaServices} from 'services/mediaServices';
5 | import {observePlaybackEnd} from './playback';
6 |
7 | if (!isMiniPlayer) {
8 | const logger = new Logger('mediaPlayback/scrobbler');
9 | const registeredScrobblers: Record = {};
10 |
11 | observePlaybackEnd()
12 | .pipe(mergeMap((state) => addListen(state)))
13 | .subscribe(logger);
14 |
15 | observeMediaServices()
16 | .pipe(
17 | mergeMap((services) => services),
18 | filter((service) => !registeredScrobblers[service.id]),
19 | tap((service) => (registeredScrobblers[service.id] = true)),
20 | tap((service) => service.scrobble?.())
21 | )
22 | .subscribe(logger);
23 | }
24 |
--------------------------------------------------------------------------------
/src/services/mediaServices/index.ts:
--------------------------------------------------------------------------------
1 | export * from './mediaServices';
2 |
--------------------------------------------------------------------------------
/src/services/mediaServices/noAuth.ts:
--------------------------------------------------------------------------------
1 | import {of} from 'rxjs';
2 | import Auth from 'types/Auth';
3 |
4 | export default function noAuth(isLoggedIn: boolean): Auth {
5 | return {
6 | noAuth: true,
7 | observeIsLoggedIn: () => of(isLoggedIn),
8 | isConnected: () => isLoggedIn,
9 | isLoggedIn: () => isLoggedIn,
10 | login: () => Promise.resolve(),
11 | logout: () => Promise.resolve(),
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/src/services/metadata/index.ts:
--------------------------------------------------------------------------------
1 | export * from './matcher';
2 | export * from './metadata';
3 | export * from './metadataChanges';
4 | export * from './music-metadata-js';
5 | export * from './playlistParser';
6 | export * from './thumbnails';
7 | export * from './userData';
8 |
--------------------------------------------------------------------------------
/src/services/mixcloud/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './mixcloud';
2 |
--------------------------------------------------------------------------------
/src/services/mixcloud/mixcloud.ts:
--------------------------------------------------------------------------------
1 | import PublicMediaService from 'types/PublicMediaService';
2 | import ServiceType from 'types/ServiceType';
3 | import noAuth from 'services/mediaServices/noAuth';
4 |
5 | const mixcloud: PublicMediaService = {
6 | ...noAuth(false),
7 | id: 'mixcloud',
8 | name: 'Mixcloud',
9 | icon: 'mixcloud',
10 | url: 'https://www.mixcloud.com',
11 | serviceType: ServiceType.PublicMedia,
12 | defaultHidden: true,
13 | defaultNoScrobble: true,
14 | iframeAudioPlayback: {
15 | // Mixcloud player shows mainly interactive content.
16 | // Use CoverArt visualizer instead.
17 | showCoverArt: true,
18 | },
19 | compareForRating: () => false,
20 | };
21 |
22 | export default mixcloud;
23 |
--------------------------------------------------------------------------------
/src/services/musicbrainz/index.ts:
--------------------------------------------------------------------------------
1 | export const musicBrainzHost = 'https://musicbrainz.org';
2 |
--------------------------------------------------------------------------------
/src/services/navidrome/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './navidrome';
2 | export * from './navidrome';
3 |
--------------------------------------------------------------------------------
/src/services/navidrome/subsonicApi.ts:
--------------------------------------------------------------------------------
1 | import SubsonicApi from 'services/subsonic/factory/SubsonicApi';
2 | import navidromeSettings from './navidromeSettings';
3 |
4 | const subsonicApi = new SubsonicApi('navidrome', navidromeSettings);
5 |
6 | export default subsonicApi;
7 |
--------------------------------------------------------------------------------
/src/services/online.ts:
--------------------------------------------------------------------------------
1 | import type {Observable} from 'rxjs';
2 | import {BehaviorSubject, distinctUntilChanged, fromEvent, map, merge} from 'rxjs';
3 |
4 | const isOnLine$ = new BehaviorSubject(isOnLine());
5 |
6 | export function observeIsOnLine(): Observable {
7 | return isOnLine$.pipe(distinctUntilChanged());
8 | }
9 |
10 | export function isOnLine(): boolean {
11 | return navigator.onLine;
12 | }
13 |
14 | merge(
15 | fromEvent(window, 'online').pipe(map(() => true)),
16 | fromEvent(window, 'offline').pipe(map(() => false))
17 | ).subscribe(isOnLine$);
18 |
--------------------------------------------------------------------------------
/src/services/pagers/ErrorPager.ts:
--------------------------------------------------------------------------------
1 | import type {Observable} from 'rxjs';
2 | import {NEVER, of} from 'rxjs';
3 | import Pager from 'types/Pager';
4 |
5 | export default class ErrorPager implements Pager {
6 | readonly pageSize = 0;
7 |
8 | constructor(private readonly error: unknown) {}
9 |
10 | observeBusy(): Observable {
11 | return of(false);
12 | }
13 |
14 | observeItems(): Observable {
15 | return NEVER;
16 | }
17 |
18 | observeSize(): Observable {
19 | return NEVER;
20 | }
21 |
22 | observeError(): Observable {
23 | return of(this.error);
24 | }
25 |
26 | fetchAt(): void {
27 | // do nothing
28 | }
29 |
30 | disconnect(): void {
31 | // do nothing
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/services/pagers/SimpleMediaPager.ts:
--------------------------------------------------------------------------------
1 | import MediaObject from 'types/MediaObject';
2 | import AbstractPager from './AbstractPager';
3 |
4 | export default class SimpleMediaPager extends AbstractPager {
5 | constructor(private readonly fetch: () => Promise) {
6 | super({pageSize: Infinity});
7 | }
8 |
9 | fetchAt(index: 0): void;
10 | fetchAt(): void {
11 | if (!this.disconnected && !this.connected) {
12 | this.connect();
13 | this.busy = true;
14 | this.fetch()
15 | .then((items) => {
16 | this.size = items.length;
17 | this.items = items;
18 | this.busy = false;
19 | })
20 | .catch((error) => {
21 | this.error = error;
22 | this.busy = false;
23 | });
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/services/pagers/SubjectPager.ts:
--------------------------------------------------------------------------------
1 | import MediaObject from 'types/MediaObject';
2 | import AbstractPager from './AbstractPager';
3 |
4 | export default class SubjectPager extends AbstractPager {
5 | fetchAt(): void {
6 | // do nothing
7 | }
8 |
9 | async next(fetch: () => Promise): Promise {
10 | if (!this.disconnected) {
11 | this.busy = true;
12 | try {
13 | this.connect();
14 | const items = await fetch();
15 | this.size = items.length;
16 | this.items = items;
17 | } catch (err) {
18 | this.error = err;
19 | }
20 | this.busy = false;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/services/playlist/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './playlist';
2 | export * from './playlist';
3 |
--------------------------------------------------------------------------------
/src/services/plex/bootstrap.ts:
--------------------------------------------------------------------------------
1 | import mediaPlayer from 'services/mediaPlayback/mediaPlayer';
2 | import plexRadioPlayer from './plexRadioPlayer';
3 |
4 | mediaPlayer.registerPlayer(plexRadioPlayer, (item) => !!item?.src.startsWith('plex:radio:'));
5 |
--------------------------------------------------------------------------------
/src/services/plex/components/PlexLogin.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {LoginProps} from 'components/Login';
3 | import DefaultLogin from 'components/Login/DefaultLogin';
4 | import usePinRefresher from './usePinRefresher';
5 |
6 | export default function PlexLogin({service: plex}: LoginProps) {
7 | usePinRefresher();
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/src/services/plex/components/usePinRefresher.ts:
--------------------------------------------------------------------------------
1 | import {useEffect} from 'react';
2 | import {Subject, map, mergeMap, tap, timer} from 'rxjs';
3 | import {refreshPin} from '../plexAuth';
4 |
5 | export default function usePinRefresher() {
6 | useEffect(() => {
7 | const delay$ = new Subject();
8 | const subscription = delay$
9 | .pipe(
10 | mergeMap((delay) => timer(delay)),
11 | mergeMap(() => refreshPin()),
12 | map((pin) => pin.expiresIn * 1000),
13 | tap((delay) => delay$.next(delay))
14 | )
15 | .subscribe();
16 | delay$.next(0);
17 | return () => subscription.unsubscribe();
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/src/services/plex/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './plex';
2 | export * from './plex';
3 |
--------------------------------------------------------------------------------
/src/services/plex/plexItemType.ts:
--------------------------------------------------------------------------------
1 | enum plexItemType {
2 | Artist = 'artist',
3 | Album = 'album',
4 | Track = 'track',
5 | Clip = 'clip',
6 | Playlist = 'playlist',
7 | }
8 |
9 | export default plexItemType;
10 |
--------------------------------------------------------------------------------
/src/services/plex/plexMediaType.ts:
--------------------------------------------------------------------------------
1 | enum plexMediaType {
2 | // Movie = '1',
3 | // Show = '2',
4 | // Season = '3',
5 | Episode = '4',
6 | // Trailer = '5',
7 | // Comic = '6',
8 | // Person = '7',
9 | Artist = '8',
10 | Album = '9',
11 | Track = '10',
12 | // PhotoAlbum = '11',
13 | MusicVideo = '12', // Or 'Picture'?
14 | // Photo = '13',
15 | Clip = '14',
16 | Playlist = '15',
17 | }
18 |
19 | export default plexMediaType;
20 |
--------------------------------------------------------------------------------
/src/services/soundcloud/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './soundcloud';
2 |
--------------------------------------------------------------------------------
/src/services/soundcloud/soundcloud.ts:
--------------------------------------------------------------------------------
1 | import PublicMediaService from 'types/PublicMediaService';
2 | import ServiceType from 'types/ServiceType';
3 | import noAuth from 'services/mediaServices/noAuth';
4 |
5 | const soundcloud: PublicMediaService = {
6 | ...noAuth(false),
7 | id: 'soundcloud',
8 | name: 'SoundCloud',
9 | icon: 'soundcloud',
10 | url: 'https://soundcloud.com',
11 | serviceType: ServiceType.PublicMedia,
12 | defaultHidden: true,
13 | iframeAudioPlayback: {
14 | showContent: true,
15 | isCoverArt: true,
16 | },
17 | compareForRating: () => false,
18 | };
19 |
20 | export default soundcloud;
21 |
--------------------------------------------------------------------------------
/src/services/spotify/bootstrap.ts:
--------------------------------------------------------------------------------
1 | import mediaPlayer from 'services/mediaPlayback/mediaPlayer';
2 | import spotifyAudioAnalyser from './spotifyAudioAnalyser';
3 | import spotifyPlayer from './spotifyPlayer';
4 |
5 | spotifyAudioAnalyser.player = spotifyPlayer;
6 |
7 | mediaPlayer.registerPlayer(spotifyPlayer, (item) => !!item?.src.startsWith('spotify:'));
8 |
--------------------------------------------------------------------------------
/src/services/spotify/components/useCredentials.ts:
--------------------------------------------------------------------------------
1 | import useObservable from 'hooks/useObservable';
2 | import spotifySettings, {SpotifyCredentials} from '../spotifySettings';
3 |
4 | export default function useCredentials(): SpotifyCredentials {
5 | return useObservable(spotifySettings.observeCredentials, spotifySettings.getCredentials());
6 | }
7 |
--------------------------------------------------------------------------------
/src/services/spotify/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './spotify';
2 | export * from './spotify';
3 |
--------------------------------------------------------------------------------
/src/services/spotify/spotifyApi.ts:
--------------------------------------------------------------------------------
1 | import SpotifyWebApi from 'spotify-web-api-js';
2 |
3 | const spotifyApi = new SpotifyWebApi();
4 |
5 | export default spotifyApi;
6 |
--------------------------------------------------------------------------------
/src/services/subsonic/airsonic.ts:
--------------------------------------------------------------------------------
1 | import SubsonicService from './factory/SubsonicService';
2 |
3 | const airsonic = new SubsonicService(
4 | 'airsonic',
5 | 'Airsonic',
6 | 'https://github.com/airsonic-advanced/airsonic-advanced',
7 | 'Airsonic-Advanced'
8 | );
9 |
10 | export default airsonic;
11 |
--------------------------------------------------------------------------------
/src/services/subsonic/ampache.ts:
--------------------------------------------------------------------------------
1 | import SubsonicService from './factory/SubsonicService';
2 |
3 | const ampache = new SubsonicService(
4 | 'ampache',
5 | 'Ampache',
6 | 'https://ampache.org',
7 | 'Ampache (Subsonic API)'
8 | );
9 |
10 | export default ampache;
11 |
--------------------------------------------------------------------------------
/src/services/subsonic/factory/components/SubsonicLoginDialog.scss:
--------------------------------------------------------------------------------
1 | .login-dialog {
2 | .icon-subsonic {
3 | filter: drop-shadow(1px 1px 0 rgba(16, 16, 16, 0.6)) !important;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/services/subsonic/gonic.ts:
--------------------------------------------------------------------------------
1 | import SubsonicService from './factory/SubsonicService';
2 |
3 | const gonic = new SubsonicService('gonic', 'gonic', 'https://github.com/sentriz/gonic');
4 |
5 | export default gonic;
6 |
--------------------------------------------------------------------------------
/src/services/subsonic/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './subsonic';
2 |
--------------------------------------------------------------------------------
/src/services/subsonic/subsonic.ts:
--------------------------------------------------------------------------------
1 | import SubsonicService from './factory/SubsonicService';
2 |
3 | const subsonic = new SubsonicService(
4 | 'subsonic',
5 | 'Subsonic',
6 | 'http://www.subsonic.org',
7 | 'Subsonic (or compatible)'
8 | );
9 |
10 | export default subsonic;
11 |
--------------------------------------------------------------------------------
/src/services/theme/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './theme';
2 | export * from './theme';
3 |
--------------------------------------------------------------------------------
/src/services/theme/themes/astronaut.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Astronaut",
3 | "fontName": "Roboto",
4 | "backgroundColor": "#000000",
5 | "textColor": "#95a5d6",
6 | "frameColor": "#141415",
7 | "frameTextColor": "#b1bdcd",
8 | "selectedBackgroundColor": "#221650",
9 | "selectedTextColor": "#ffffff",
10 | "spacing": 0.72,
11 | "roundness": 0.57,
12 | "flat": false
13 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/blackgold.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Black Gold",
3 | "fontName": "Inter",
4 | "backgroundColor": "#0d0c0c",
5 | "textColor": "#a9aa92",
6 | "frameColor": "#1c1c12",
7 | "frameTextColor": "#e8e8bf",
8 | "selectedBackgroundColor": "#46462d",
9 | "selectedTextColor": "#ffffff",
10 | "spacing": 0.375,
11 | "roundness": 0.25,
12 | "flat": false
13 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/carbon.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Carbon",
3 | "fontName": "Muli",
4 | "backgroundColor": "#252429",
5 | "textColor": "#dbe5eb",
6 | "frameColor": "#2f2e33",
7 | "frameTextColor": "#d1d1d1",
8 | "selectedBackgroundColor": "#244b70",
9 | "selectedTextColor": "#ffffff",
10 | "scrollbarThickness": 1,
11 | "mediaButtonColor": "#a5adbb",
12 | "mediaButtonTextColor": "#000000",
13 | "spacing": 0.38,
14 | "roundness": 0.67,
15 | "flat": false
16 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/contrast.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Contrast",
3 | "fontName": "Verdana",
4 | "backgroundColor": "#051115",
5 | "textColor": "#ddeff3",
6 | "frameColor": "#c3c3c6",
7 | "frameTextColor": "#000000",
8 | "selectedBackgroundColor": "#1e25ae",
9 | "selectedTextColor": "#ffffff",
10 | "scrollbarColor": "#85858a",
11 | "scrollbarTextColor": "#000000",
12 | "scrollbarThickness": 1.33,
13 | "mediaButtonColor": "#202037",
14 | "mediaButtonTextColor": "#f8f8fc",
15 | "spacing": 0.75,
16 | "roundness": 0.85,
17 | "flat": true
18 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/debug.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Debug",
3 | "fontName": "Cursive",
4 | "backgroundColor": "#9dceec",
5 | "textColor": "#0e0425",
6 | "frameColor": "#00af4d",
7 | "frameTextColor": "#fbf6ac",
8 | "selectedBackgroundColor": "#ffcd03",
9 | "selectedTextColor": "#000000",
10 | "buttonColor": "#f57d20",
11 | "buttonTextColor": "#000000",
12 | "scrollbarColor": "#dd1a21",
13 | "scrollbarTextColor": "#fff579",
14 | "mediaButtonColor": "#f4f4f4",
15 | "mediaButtonTextColor": "#181818",
16 | "spacing": 0.38,
17 | "roundness": 0.48,
18 | "flat": false
19 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "(boring default)",
3 | "fontName": "Arial",
4 | "backgroundColor": "#0d0f12",
5 | "textColor": "#8facbc",
6 | "frameColor": "#32312f",
7 | "frameTextColor": "#d1d1d1",
8 | "selectedBackgroundColor": "#163d5a",
9 | "selectedTextColor": "#ebebeb",
10 | "spacing": 0.38,
11 | "roundness": 0.48,
12 | "flat": false
13 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/glacier.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Glacier",
3 | "fontName": "Inter",
4 | "backgroundColor": "#d9e6e8",
5 | "textColor": "#08191b",
6 | "frameColor": "#bfd4e8",
7 | "frameTextColor": "#1c1c12",
8 | "selectedBackgroundColor": "#ffffff",
9 | "selectedTextColor": "#04030c",
10 | "spacing": 0.65,
11 | "roundness": 0.3,
12 | "flat": false
13 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/indigo.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Indigo",
3 | "fontName": "Poppins",
4 | "backgroundColor": "#06181e",
5 | "textColor": "#d1d3f5",
6 | "frameColor": "#43477a",
7 | "frameTextColor": "#f3f3f7",
8 | "selectedBackgroundColor": "#4a437a",
9 | "selectedTextColor": "#ffffff",
10 | "spacing": 0.625,
11 | "roundness": 0.84375,
12 | "flat": true
13 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/jeep.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Jeep",
3 | "fontName": "Overpass",
4 | "backgroundColor": "#101202",
5 | "textColor": "#8ebf88",
6 | "frameColor": "#1c2103",
7 | "frameTextColor": "#cbcdb1",
8 | "selectedBackgroundColor": "#362802",
9 | "selectedTextColor": "#ffffff",
10 | "scrollbarThickness": 1,
11 | "spacing": 0.72,
12 | "roundness": 0.84,
13 | "flat": false
14 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/lego.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "LEGO",
3 | "fontName": "Open Sans",
4 | "backgroundColor": "#fcfcfc",
5 | "textColor": "#152332",
6 | "frameColor": "#1b80f1",
7 | "frameTextColor": "#ffffff",
8 | "selectedBackgroundColor": "#fc2420",
9 | "selectedTextColor": "#ffffff",
10 | "mediaButtonColor": "#f4c522",
11 | "mediaButtonTextColor": "#12100d",
12 | "spacing": 0.53,
13 | "roundness": 0,
14 | "flat": true
15 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/mellowyellow.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Mellow Yellow",
3 | "fontName": "Poppins",
4 | "backgroundColor": "#f5f2d1",
5 | "textColor": "#1c1302",
6 | "frameColor": "#fef4c3",
7 | "frameTextColor": "#513506",
8 | "selectedBackgroundColor": "#fdeb91",
9 | "selectedTextColor": "#000000",
10 | "mediaButtonColor": "#fffae1",
11 | "mediaButtonTextColor": "#513506",
12 | "spacing": 0.32,
13 | "roundness": 0.29,
14 | "flat": true
15 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/moodyblue.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Moody Blue",
3 | "fontName": "Inter",
4 | "backgroundColor": "#051a23",
5 | "textColor": "#bec1c0",
6 | "frameColor": "#125673",
7 | "frameTextColor": "#d8eee7",
8 | "selectedBackgroundColor": "#124373",
9 | "selectedTextColor": "#ffffff",
10 | "spacing": 0.5,
11 | "roundness": 1,
12 | "flat": false
13 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/neon.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Neon",
3 | "fontName": "Roboto",
4 | "backgroundColor": "#010123",
5 | "textColor": "#7edbdd",
6 | "frameColor": "#610a51",
7 | "frameTextColor": "#fec3f3",
8 | "selectedBackgroundColor": "#28449a",
9 | "selectedTextColor": "#ffffff",
10 | "mediaButtonColor": "#f2d202",
11 | "mediaButtonTextColor": "#160914",
12 | "spacing": 0.59,
13 | "roundness": 0.62,
14 | "flat": false
15 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/notebook.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Notebook",
3 | "fontName": "Muli",
4 | "backgroundColor": "#f7f7f7",
5 | "textColor": "#150e0e",
6 | "frameColor": "#d1d1d1",
7 | "frameTextColor": "#121111",
8 | "selectedBackgroundColor": "#feffbd",
9 | "selectedTextColor": "#000000",
10 | "mediaButtonColor": "#c5e0e7",
11 | "mediaButtonTextColor": "#1b121c",
12 | "spacing": 0.48,
13 | "roundness": 0.14,
14 | "flat": true
15 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/palepink.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Pale Pink",
3 | "fontName": "Muli",
4 | "backgroundColor": "#f7f7f7",
5 | "textColor": "#150e0e",
6 | "frameColor": "#cfbdd0",
7 | "frameTextColor": "#121111",
8 | "selectedBackgroundColor": "#b9d8f3",
9 | "selectedTextColor": "#000000",
10 | "spacing": 0.375,
11 | "roundness": 0.53125,
12 | "flat": true
13 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/potpourri.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Potpourri",
3 | "fontName": "Bricolage Grotesque",
4 | "backgroundColor": "#f9f8dd",
5 | "textColor": "#000000",
6 | "frameColor": "#beaee0",
7 | "frameTextColor": "#000000",
8 | "selectedBackgroundColor": "#fcccc2",
9 | "selectedTextColor": "#000000",
10 | "mediaButtonColor": "#fcccc2",
11 | "mediaButtonTextColor": "#000000",
12 | "spacing": 0.38,
13 | "roundness": 0.67,
14 | "flat": true
15 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/proton.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Proton",
3 | "fontName": "System UI",
4 | "backgroundColor": "#120f0c",
5 | "textColor": "#c4c7b8",
6 | "frameColor": "#121211",
7 | "frameTextColor": "#72bba4",
8 | "selectedBackgroundColor": "#27493f",
9 | "selectedTextColor": "#e6e8de",
10 | "mediaButtonColor": "#34836a",
11 | "mediaButtonTextColor": "#10100b",
12 | "buttonColor": "#292924",
13 | "buttonTextColor": "#72bba4",
14 | "scrollbarThickness": 0.67,
15 | "spacing": 0.375,
16 | "roundness": 0.53125,
17 | "flat": true
18 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/purplelicious.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Purplelicious (Winamp)",
3 | "fontName": "Arial",
4 | "backgroundColor": "#000000",
5 | "textColor": "#ffb464",
6 | "frameColor": "#3e0445",
7 | "frameTextColor": "#ffb464",
8 | "selectedBackgroundColor": "#3e0445",
9 | "selectedTextColor": "#ffb464",
10 | "spacing": 0.21875,
11 | "roundness": 0.09375,
12 | "flat": false
13 | }
14 |
--------------------------------------------------------------------------------
/src/services/theme/themes/radioactive.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Radioactive",
3 | "fontName": "Ubuntu",
4 | "backgroundColor": "#0f0f10",
5 | "textColor": "#d1f5d2",
6 | "frameColor": "#322724",
7 | "frameTextColor": "#c3feca",
8 | "selectedBackgroundColor": "#43304b",
9 | "selectedTextColor": "#ffffff",
10 | "mediaButtonColor": "#c3feca",
11 | "mediaButtonTextColor": "#021c05",
12 | "spacing": 0.73,
13 | "roundness": 0.86,
14 | "flat": false
15 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/saddle.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Saddle",
3 | "fontName": "Bricolage Grotesque",
4 | "backgroundColor": "#271607",
5 | "textColor": "#cbbe86",
6 | "frameColor": "#331d0a",
7 | "frameTextColor": "#fedfc3",
8 | "selectedBackgroundColor": "#5d3512",
9 | "selectedTextColor": "#ffffff",
10 | "spacing": 0.375,
11 | "roundness": 0.34375,
12 | "flat": false
13 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/treasure.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Treasure",
3 | "fontName": "Nunito Sans",
4 | "backgroundColor": "#040323",
5 | "textColor": "#dee5e8",
6 | "frameColor": "#64211c",
7 | "frameTextColor": "#e4d8d8",
8 | "selectedBackgroundColor": "#373c0c",
9 | "selectedTextColor": "#ffffff",
10 | "mediaButtonColor": "#d1ca10",
11 | "mediaButtonTextColor": "#12100d",
12 | "spacing": 0.43,
13 | "roundness": 0.69,
14 | "flat": false
15 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/velvet.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Velvet",
3 | "fontName": "Lato",
4 | "backgroundColor": "#160916",
5 | "textColor": "#c2d7ea",
6 | "frameColor": "#280b1f",
7 | "frameTextColor": "#7de8e6",
8 | "mediaButtonColor": "#ccae14",
9 | "mediaButtonTextColor": "#0d0b04",
10 | "selectedBackgroundColor": "#412438",
11 | "selectedTextColor": "#ebebeb",
12 | "scrollbarTextColor": "#d7bd3c",
13 | "spacing": 0.85,
14 | "roundness": 0.67,
15 | "scrollbarThickness": 1.33,
16 | "flat": true
17 | }
--------------------------------------------------------------------------------
/src/services/theme/themes/winamp-classic.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Winamp Classic",
3 | "fontName": "Arial",
4 | "backgroundColor": "#000000",
5 | "textColor": "#00e200",
6 | "frameColor": "#383757",
7 | "frameTextColor": "#d2d2d2",
8 | "buttonColor": "#464379",
9 | "buttonTextColor": "#d2d2d2",
10 | "mediaButtonColor": "#cccccc",
11 | "mediaButtonTextColor": "#111111",
12 | "selectedBackgroundColor": "#040dae",
13 | "selectedTextColor": "#d2d2d2",
14 | "spacing": 0.21875,
15 | "roundness": 0.09375,
16 | "scrollbarThickness": 0.67,
17 | "flat": false
18 | }
19 |
--------------------------------------------------------------------------------
/src/services/theme/themes/winamp-modern.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Winamp Modern (WACUP)",
3 | "fontName": "Inter",
4 | "backgroundColor": "#14121c",
5 | "textColor": "#8ac440",
6 | "frameColor": "#1f1f36",
7 | "frameTextColor": "#f0f0f0",
8 | "mediaButtonColor": "#cccccc",
9 | "mediaButtonTextColor": "#111111",
10 | "selectedBackgroundColor": "#2f2f76",
11 | "selectedTextColor": "#f0f0f0",
12 | "spacing": 0.21875,
13 | "roundness": 0.09375,
14 | "flat": false
15 | }
16 |
--------------------------------------------------------------------------------
/src/services/tidal/bootstrap.ts:
--------------------------------------------------------------------------------
1 | import mediaPlayer from 'services/mediaPlayback/mediaPlayer';
2 | import tidalPlayer from './tidalPlayer';
3 |
4 | mediaPlayer.registerPlayer(tidalPlayer, (item) => !!item?.src.startsWith('tidal:'));
5 |
--------------------------------------------------------------------------------
/src/services/tidal/components/useCredentials.ts:
--------------------------------------------------------------------------------
1 | import useObservable from 'hooks/useObservable';
2 | import tidalSettings, {TidalCredentials} from '../tidalSettings';
3 |
4 | export default function useCredentials(): TidalCredentials {
5 | return useObservable(tidalSettings.observeCredentials, tidalSettings.getCredentials());
6 | }
7 |
--------------------------------------------------------------------------------
/src/services/tidal/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './tidal';
2 | export * from './tidal';
3 |
--------------------------------------------------------------------------------
/src/services/visualizer/ambientvideo/ambientvideo.ts:
--------------------------------------------------------------------------------
1 | import VisualizerProvider from 'types/VisualizerProvider';
2 | import {AmbientVideoVisualizer} from 'types/Visualizer';
3 | import AmbientVideoPlayer from './AmbientVideoPlayer';
4 | import {getVisualizers, observeVisualizers} from './visualizers';
5 |
6 | const ambientvideo: VisualizerProvider = {
7 | id: 'ambientvideo',
8 | name: 'Ambient Video',
9 | defaultHidden: true,
10 | get visualizers() {
11 | return getVisualizers();
12 | },
13 | observeVisualizers,
14 | createPlayer(audio) {
15 | if (!this.player) {
16 | (this as any).player = new AmbientVideoPlayer(audio);
17 | }
18 | return this.player!;
19 | },
20 | };
21 |
22 | export default ambientvideo;
23 |
--------------------------------------------------------------------------------
/src/services/visualizer/ambientvideo/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './ambientvideo';
2 | export * from './ambientvideo';
3 |
--------------------------------------------------------------------------------
/src/services/visualizer/ampshader/ampshader.ts:
--------------------------------------------------------------------------------
1 | import VisualizerProvider from 'types/VisualizerProvider';
2 | import {AmpShaderVisualizer} from 'types/Visualizer';
3 | import AmpShaderPlayer from './AmpShaderPlayer';
4 | import {getVisualizers, observeVisualizers} from './visualizers';
5 |
6 | const ampshader: VisualizerProvider = {
7 | id: 'ampshader',
8 | name: 'ampshader',
9 | get visualizers() {
10 | return getVisualizers();
11 | },
12 | observeVisualizers,
13 | createPlayer(audio) {
14 | if (!this.player) {
15 | (this as any).player = new AmpShaderPlayer(audio, 'main');
16 | }
17 | return this.player!;
18 | },
19 | };
20 |
21 | export default ampshader;
22 |
--------------------------------------------------------------------------------
/src/services/visualizer/ampshader/footer.frag:
--------------------------------------------------------------------------------
1 | out vec4 as_FragColor;
2 | void main() {
3 | vec4 color = vec4(1e20);
4 | mainImage(color, gl_FragCoord.xy);
5 | as_FragColor = color;
6 | }
7 |
--------------------------------------------------------------------------------
/src/services/visualizer/ampshader/header.frag:
--------------------------------------------------------------------------------
1 | #version 300 es
2 | #ifdef GL_OES_standard_derivatives
3 | #extension GL_OES_standard_derivatives : enable
4 | #endif
5 | #ifdef GL_ES
6 | precision highp float;
7 | precision highp int;
8 | precision mediump sampler3D;
9 | #endif
10 | uniform vec3 iBackgroundColor;
11 | uniform vec3 iBlackColor;
12 | uniform vec3 iColor;
13 | uniform vec3 iFrameColor;
14 | uniform vec3 iResolution;
15 | uniform float iTime;
16 | uniform float iTimeDelta;
17 | uniform int iFrame;
18 | uniform float iChannelTime[4];
19 | uniform vec3 iMouse;
20 | uniform sampler2D iChannel0;
21 | uniform sampler2D iChannel1;
22 | uniform sampler2D iChannel2;
23 | uniform sampler2D iChannel3;
24 | uniform vec4 iDate;
25 |
--------------------------------------------------------------------------------
/src/services/visualizer/ampshader/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './ampshader';
2 | export * from './ampshader';
3 |
--------------------------------------------------------------------------------
/src/services/visualizer/ampshader/visualizers/creation.frag:
--------------------------------------------------------------------------------
1 | // https://noisehack.com/build-music-visualizer-web-audio-api/ (adapted from)
2 | // https://www.shadertoy.com/view/XsXXDn
3 | // http://www.pouet.net/prod.php?which=57245
4 | // If you intend to reuse this shader, please add credits to 'Danilo Guanabara'
5 |
6 | void mainImage( out vec4 fragColor, in vec2 fragCoord ){
7 | vec3 c;
8 | float z = 0.1 * iTime;
9 | vec2 uv = fragCoord.xy / iResolution.xy;
10 | vec2 p = uv - 0.5;
11 | p.x *= iResolution.x / iResolution.y;
12 | float l = 0.2 * length(p);
13 | for (int i = 0; i < 3; i++) {
14 | z += 0.07;
15 | uv += p / l * (sin(z) + 1.0) * abs(sin(l * 9.0 - z * 2.0));
16 | c[i] = 0.01 / length(abs(mod(uv, 1.0) - 0.5));
17 | }
18 | float intensity = texture(iChannel0, vec2(l, 0.5)).x;
19 | fragColor = vec4(c / l * intensity, iTime);
20 | }
21 |
--------------------------------------------------------------------------------
/src/services/visualizer/ampshader/visualizers/index.ts:
--------------------------------------------------------------------------------
1 | import type {Observable} from 'rxjs';
2 | import {BehaviorSubject} from 'rxjs';
3 | import {AmpShaderVisualizer} from 'types/Visualizer';
4 |
5 | const visualizers$ = new BehaviorSubject([]);
6 |
7 | export function getVisualizers(): readonly AmpShaderVisualizer[] {
8 | return visualizers$.value;
9 | }
10 |
11 | export function observeVisualizers(): Observable {
12 | return visualizers$;
13 | }
14 |
15 | async function loadVisualizers(): Promise {
16 | const {default: presets} = await import(
17 | /* webpackChunkName: "ampshader-presets" */
18 | /* webpackMode: "lazy-once" */
19 | './presets'
20 | );
21 | visualizers$.next(presets);
22 | }
23 |
24 | loadVisualizers();
25 |
--------------------------------------------------------------------------------
/src/services/visualizer/ampshader/visualizers/soundSinusWave.frag:
--------------------------------------------------------------------------------
1 | // https://www.shadertoy.com/view/XsX3zS
2 | #define WAVES 8.0
3 |
4 | void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
5 | vec2 uv = -1.0 + 2.0 * fragCoord.xy / iResolution.xy;
6 |
7 | float time = iTime * 1.0;
8 |
9 | vec3 color = vec3(0.0);
10 |
11 | for (float i=0.0; i 0.5)
6 | {
7 | uv -= 0.5;
8 | uv *= 2.;
9 | }
10 | else
11 | {
12 | uv *= 2.;
13 | uv = 1. - uv;
14 | }
15 |
16 | float fft = texture( iChannel0, vec2(uv.x,0.25) ).x;
17 | float dr = length(uv);
18 | float radius = 1.8;
19 |
20 | vec3 col = vec3(0.);
21 | if( abs(uv.y) = {
8 | id: 'audiomotion',
9 | name: 'audioMotion-analyzer',
10 | shortName: 'audioMotion',
11 | externalUrl: 'https://audiomotion.dev/',
12 | visualizers,
13 | observeVisualizers: () => of(visualizers),
14 | createPlayer(audio) {
15 | if (!this.player) {
16 | (this as any).player = new AudioMotionPlayer(audio);
17 | }
18 | return this.player!;
19 | },
20 | };
21 |
22 | export default audiomotion;
23 |
--------------------------------------------------------------------------------
/src/services/visualizer/audiomotion/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './audiomotion';
2 | export * from './audiomotion';
3 |
--------------------------------------------------------------------------------
/src/services/visualizer/butterchurn/butterchurn.ts:
--------------------------------------------------------------------------------
1 | import VisualizerProvider from 'types/VisualizerProvider';
2 | import {ButterchurnVisualizer} from 'types/Visualizer';
3 | import ButterchurnPlayer from './ButterchurnPlayer';
4 | import {getVisualizers, observeVisualizers} from './visualizers';
5 |
6 | const butterchurn: VisualizerProvider = {
7 | id: 'butterchurn',
8 | name: 'Butterchurn (Milkdrop)',
9 | shortName: 'Butterchurn',
10 | externalUrl: 'https://butterchurnviz.com/',
11 | get visualizers() {
12 | return getVisualizers();
13 | },
14 | observeVisualizers,
15 | createPlayer(audio) {
16 | if (!this.player) {
17 | (this as any).player = new ButterchurnPlayer(audio);
18 | }
19 | return this.player!;
20 | },
21 | };
22 |
23 | export default butterchurn;
24 |
--------------------------------------------------------------------------------
/src/services/visualizer/butterchurn/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './butterchurn';
2 | export * from './butterchurn';
3 |
--------------------------------------------------------------------------------
/src/services/visualizer/coverart/AnimatedBackgroundPlayer.ts:
--------------------------------------------------------------------------------
1 | import AudioManager from 'types/AudioManager';
2 | import {AmpShaderVisualizer} from 'types/Visualizer';
3 | import AmpShaderPlayer from '../ampshader/AmpShaderPlayer';
4 | import animatedBackground from './animatedBackground.frag';
5 |
6 | export default class AnimatedBackgroundPlayer extends AmpShaderPlayer {
7 | private readonly visualizer: AmpShaderVisualizer = {
8 | providerId: 'ampshader',
9 | name: 'Foamy water by k_mouse',
10 | shader: animatedBackground,
11 | externalUrl: 'https://www.shadertoy.com/view/llcXW7',
12 | };
13 |
14 | constructor(audio: AudioManager) {
15 | super(audio, 'animated-background');
16 | }
17 |
18 | load(): void {
19 | super.load(this.visualizer);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/services/visualizer/coverart/components/useNextTrack.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import PlaylistItem from 'types/PlaylistItem';
3 | import {observeNextItem, getNextItem} from 'services/playlist';
4 |
5 | export default function useNextTrack(): PlaylistItem | null {
6 | const [value, setValue] = useState(getNextItem());
7 |
8 | useEffect(() => {
9 | const subscription = observeNextItem().subscribe(setValue);
10 | return () => subscription.unsubscribe();
11 | }, []);
12 |
13 | return value;
14 | }
15 |
--------------------------------------------------------------------------------
/src/services/visualizer/coverart/coverart.ts:
--------------------------------------------------------------------------------
1 | import {of} from 'rxjs';
2 | import VisualizerProvider from 'types/VisualizerProvider';
3 | import {CoverArtVisualizer} from 'types/Visualizer';
4 | import CovertArtPlayer from './CovertArtPlayer';
5 | import visualizers from './visualizers';
6 |
7 | const coverart: VisualizerProvider = {
8 | id: 'coverart',
9 | name: 'Cover Art',
10 | visualizers,
11 | observeVisualizers: () => of(visualizers),
12 | createPlayer(audio) {
13 | if (!this.player) {
14 | (this as any).player = new CovertArtPlayer(audio);
15 | }
16 | return this.player!;
17 | },
18 | };
19 |
20 | export default coverart;
21 |
--------------------------------------------------------------------------------
/src/services/visualizer/coverart/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './coverart';
2 | export * from './coverart';
3 |
--------------------------------------------------------------------------------
/src/services/visualizer/coverart/visualizers/index.ts:
--------------------------------------------------------------------------------
1 | import {CoverArtVisualizer} from 'types/Visualizer';
2 | import {default as DefaultVisualizer} from '../components/CoverArtVisualizer';
3 |
4 | const defaultVisualizer: CoverArtVisualizer = {
5 | providerId: 'coverart',
6 | name: '',
7 | component: DefaultVisualizer,
8 | };
9 |
10 | const visualizers: CoverArtVisualizer[] = [defaultVisualizer];
11 |
12 | export default visualizers;
13 |
--------------------------------------------------------------------------------
/src/services/visualizer/index.ts:
--------------------------------------------------------------------------------
1 | export * from './visualizer';
2 | export {observeVisualizerSettings} from './visualizerSettings';
3 |
--------------------------------------------------------------------------------
/src/services/visualizer/spotifyviz/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './spotifyviz';
2 | export * from './spotifyviz';
3 |
--------------------------------------------------------------------------------
/src/services/visualizer/spotifyviz/spotifyviz.ts:
--------------------------------------------------------------------------------
1 | import {of} from 'rxjs';
2 | import VisualizerProvider from 'types/VisualizerProvider';
3 | import {SpotifyVizVisualizer} from 'types/Visualizer';
4 | import SpotifyVizPlayer from './SpotifyVizPlayer';
5 | import visualizers from './visualizers';
6 |
7 | const spotifyviz: VisualizerProvider = {
8 | id: 'spotifyviz',
9 | name: 'spotify-viz',
10 | // 404 now (used to work)
11 | // externalUrl: 'https://github.com/zachwinter/spotify-viz/',
12 | visualizers,
13 | observeVisualizers: () => of(visualizers),
14 | createPlayer() {
15 | if (!this.player) {
16 | (this as any).player = new SpotifyVizPlayer();
17 | }
18 | return this.player!;
19 | },
20 | };
21 |
22 | export default spotifyviz;
23 |
--------------------------------------------------------------------------------
/src/services/visualizer/spotifyviz/visualizers/index.ts:
--------------------------------------------------------------------------------
1 | import {SpotifyVizVisualizer} from 'types/Visualizer';
2 | import example from './example';
3 |
4 | const visualizers: SpotifyVizVisualizer[] = [
5 | {
6 | providerId: 'spotifyviz',
7 | name: 'example by zachwinter',
8 | config: example,
9 | // Dead link.
10 | // externalUrl: 'https://github.com/zachwinter/spotify-viz/blob/master/client/example.js',
11 | },
12 | ];
13 |
14 | export default visualizers;
15 |
--------------------------------------------------------------------------------
/src/services/visualizer/visualizers.ts:
--------------------------------------------------------------------------------
1 | import VisualizerProvider from 'types/VisualizerProvider';
2 | import ambientvideo from './ambientvideo';
3 | import ampshader from './ampshader';
4 | import audiomotion from './audiomotion';
5 | import butterchurn from './butterchurn';
6 | import coverart from './coverart';
7 | import spotifyviz from './spotifyviz';
8 | import waveform from './waveform';
9 |
10 | const visualizers: VisualizerProvider[] = [
11 | ambientvideo,
12 | ampshader,
13 | audiomotion,
14 | butterchurn,
15 | coverart,
16 | spotifyviz,
17 | waveform
18 | ];
19 |
20 | export default visualizers;
21 |
--------------------------------------------------------------------------------
/src/services/visualizer/waveform/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './waveform';
2 | export * from './waveform';
3 |
--------------------------------------------------------------------------------
/src/services/visualizer/waveform/waveform.ts:
--------------------------------------------------------------------------------
1 | import {of} from 'rxjs';
2 | import VisualizerProvider from 'types/VisualizerProvider';
3 | import {WaveformVisualizer} from 'types/Visualizer';
4 | import WaveformPlayer from './WaveformPlayer';
5 | import visualizers from './visualizers';
6 |
7 | const waveform: VisualizerProvider = {
8 | id: 'waveform',
9 | name: 'Waveform',
10 | externalUrl:
11 | 'https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API',
12 | visualizers,
13 | observeVisualizers: () => of(visualizers),
14 | createPlayer(audio) {
15 | if (!this.player) {
16 | (this as any).player = new WaveformPlayer(audio, 'main');
17 | }
18 | return this.player!;
19 | },
20 | };
21 |
22 | export default waveform;
23 |
--------------------------------------------------------------------------------
/src/services/youtube/components/useCredentials.ts:
--------------------------------------------------------------------------------
1 | import useObservable from 'hooks/useObservable';
2 | import youtubeSettings, {YouTubeCredentials} from '../youtubeSettings';
3 |
4 | export default function useCredentials(): YouTubeCredentials {
5 | return useObservable(youtubeSettings.observeCredentials, youtubeSettings.getCredentials());
6 | }
7 |
--------------------------------------------------------------------------------
/src/services/youtube/components/useGoogleClientLibrary.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import {defer} from 'rxjs';
3 | import {getGsiClient} from '../youtubeAuth';
4 |
5 | export default function useGoogleClientLibrary() {
6 | const [client, setClient] = useState(null);
7 | const [error, setError] = useState(null);
8 |
9 | useEffect(() => {
10 | const subscription = defer(() => getGsiClient()).subscribe({
11 | next: setClient,
12 | error: setError,
13 | });
14 | return () => subscription.unsubscribe();
15 | }, []);
16 |
17 | return {client, error};
18 | }
19 |
--------------------------------------------------------------------------------
/src/services/youtube/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './youtube';
2 | export * from './youtube';
3 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @use './reset.scss';
2 | @use './theming.scss';
3 | @use './base.scss';
4 | @use './forms.scss';
5 |
--------------------------------------------------------------------------------
/src/styles/reset.scss:
--------------------------------------------------------------------------------
1 | @use './libs/minireset.scss';
2 |
3 | *:focus,
4 | *:focus-visible {
5 | outline: none;
6 | }
7 |
8 | html, body {
9 | overflow: hidden;
10 | }
11 |
--------------------------------------------------------------------------------
/src/types/Action.ts:
--------------------------------------------------------------------------------
1 | const enum Action {
2 | Pin = 'pin',
3 | Unpin = 'unpin',
4 | Like = 'like',
5 | Unlike = 'unlike',
6 | Rate = 'rate',
7 | Info = 'info',
8 | AddToLibrary = 'library-add',
9 | RemoveFromLibrary = 'library-remove',
10 | PlayNow = 'play-now',
11 | PlayNext = 'play-next',
12 | Queue = 'queue',
13 | AddToPlaylist = 'add-to-playlist',
14 | AddToNewPlaylist = 'add-to-new-playlist',
15 | AddToRecentPlaylist1 = 'add-to-recent-playlist-1',
16 | AddToRecentPlaylist2 = 'add-to-recent-playlist-2',
17 | AddToRecentPlaylist3 = 'add-to-recent-playlist-3',
18 | AddToRecentPlaylist4 = 'add-to-recent-playlist-4',
19 | }
20 |
21 | export default Action;
22 |
--------------------------------------------------------------------------------
/src/types/AmpcastElectron.d.ts:
--------------------------------------------------------------------------------
1 | export default interface AmpcastElectron {
2 | getCredential(key: string): Promise;
3 | setCredential(key: string, value: string): Promise;
4 | clearCredentials(): Promise;
5 | setFontSize(fontSize: number): void;
6 | setFrameColor(color: string): void;
7 | setFrameTextColor(color: string): void;
8 | getPreferredPort(): Promise;
9 | setPreferredPort(port: number): Promise;
10 | quit(): void;
11 | }
12 |
--------------------------------------------------------------------------------
/src/types/AudioManager.d.ts:
--------------------------------------------------------------------------------
1 | export default interface AudioManager {
2 | readonly context: AudioContext;
3 | readonly replayGain: number;
4 | readonly source: AudioNode;
5 | readonly streamingSupported: boolean;
6 | volume: number;
7 | }
8 |
--------------------------------------------------------------------------------
/src/types/AudioSettings.d.ts:
--------------------------------------------------------------------------------
1 | export default interface AudioSettings {
2 | replayGainMode: ReplayGainMode;
3 | replayGainPreAmp: number;
4 | }
5 |
--------------------------------------------------------------------------------
/src/types/Auth.d.ts:
--------------------------------------------------------------------------------
1 | import type {Observable} from 'rxjs';
2 |
3 | export default interface Auth {
4 | observeIsLoggedIn(this: unknown): Observable;
5 | isConnected(this: unknown): boolean;
6 | isLoggedIn(this: unknown): boolean;
7 | login(this: unknown): Promise;
8 | logout(this: unknown): Promise;
9 | // Everything below here should be optional.
10 | noAuth?: boolean;
11 | observeConnectionLogging?(this: unknown): Observable;
12 | reconnect?(this: unknown): void;
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/BaseVisualizer.d.ts:
--------------------------------------------------------------------------------
1 | import Thumbnail from './Thumbnail';
2 | import VisualizerProviderId from './VisualizerProviderId';
3 |
4 | export default interface BaseVisualizer {
5 | readonly providerId: VisualizerProviderId;
6 | readonly name: string;
7 | // Everything below here should be optional.
8 | readonly title?: string; // Readable title
9 | readonly externalUrl?: string;
10 | readonly thumbnails?: Thumbnail[];
11 | readonly duration?: number;
12 | readonly spotifyExcluded?: boolean;
13 | readonly opaque?: boolean;
14 | }
15 |
--------------------------------------------------------------------------------
/src/types/Browsable.d.ts:
--------------------------------------------------------------------------------
1 | import {SetRequired} from 'type-fest';
2 | import MediaService from './MediaService';
3 |
4 | type Browsable = SetRequired;
5 |
6 | export default Browsable;
7 |
--------------------------------------------------------------------------------
/src/types/BuildConfig.d.ts:
--------------------------------------------------------------------------------
1 | import MediaService from './MediaService';
2 | import MediaServiceId, {PersonalMediaServiceId} from './MediaServiceId';
3 | import PersonalMediaService from './PersonalMediaService';
4 |
5 | export default interface BuildConfig {
6 | readonly enabledServices: readonly string[];
7 | getServerHost(service: PersonalMediaService): string;
8 | getServerHost(serviceId: PersonalMediaServiceId): string;
9 | hasProxyLogin(service: PersonalMediaService): boolean;
10 | hasProxyLogin(serviceId: PersonalMediaServiceId): boolean;
11 | isStartupService(service: MediaService): boolean;
12 | isStartupService(serviceId: MediaServiceId): boolean;
13 | isServiceDisabled(service: MediaService): boolean;
14 | isServiceDisabled(serviceId: MediaServiceId): boolean;
15 | }
16 |
--------------------------------------------------------------------------------
/src/types/ChildOf.d.ts:
--------------------------------------------------------------------------------
1 | import MediaAlbum from './MediaAlbum';
2 | import MediaArtist from './MediaArtist';
3 | import MediaFolder from './MediaFolder';
4 | import MediaFolderItem from './MediaFolderItem';
5 | import MediaItem from './MediaItem';
6 | import MediaObject from './MediaObject';
7 | import MediaPlaylist from './MediaPlaylist';
8 |
9 | type ChildOf = T extends MediaArtist
10 | ? MediaAlbum
11 | : T extends MediaFolder
12 | ? MediaFolderItem
13 | : T extends MediaAlbum | MediaPlaylist
14 | ? MediaItem
15 | : never;
16 |
17 | export default ChildOf;
18 |
--------------------------------------------------------------------------------
/src/types/CreatePlaylistOptions.d.ts:
--------------------------------------------------------------------------------
1 | import MediaItem from './MediaItem';
2 |
3 | export default interface CreatePlaylistOptions {
4 | description?: string;
5 | items?: readonly T[];
6 | isPublic?: boolean;
7 | }
8 |
--------------------------------------------------------------------------------
/src/types/DRMInfo.d.ts:
--------------------------------------------------------------------------------
1 | import DRMKeySystem from './DRMKeySystem';
2 | import DRMType from './DRMType';
3 |
4 | export default interface DRMInfo {
5 | readonly type: DRMType;
6 | readonly keySystem: DRMKeySystem;
7 | readonly certificate?: string;
8 | readonly license?: string;
9 | }
10 |
--------------------------------------------------------------------------------
/src/types/DRMKeySystem.d.ts:
--------------------------------------------------------------------------------
1 | type DRMKeySystem = 'com.widevine.alpha' | 'com.apple.fairplay' | 'com.microsoft.playready';
2 |
3 | export default DRMKeySystem;
4 |
--------------------------------------------------------------------------------
/src/types/DRMType.d.ts:
--------------------------------------------------------------------------------
1 | type DRMType = 'widevine' | 'fairplay' | 'playready';
2 |
3 | export default DRMType;
4 |
--------------------------------------------------------------------------------
/src/types/DataService.ts:
--------------------------------------------------------------------------------
1 | import BaseMediaService from './BaseMediaService';
2 | import ServiceType from './ServiceType';
3 |
4 | export default interface DataService extends BaseMediaService {
5 | readonly serviceType: ServiceType.DataService;
6 | // Everything below here should be optional.
7 | readonly canScrobble?: boolean;
8 | }
9 |
--------------------------------------------------------------------------------
/src/types/ErrorReport.d.ts:
--------------------------------------------------------------------------------
1 | import Snapshot from './Snapshot';
2 |
3 | export default interface ErrorReport {
4 | readonly reportedBy: string;
5 | readonly reportingId: string; // id of logger or `MediaSource` id
6 | readonly error: {
7 | readonly message: string;
8 | readonly httpStatus?: number;
9 | readonly httpStatusText?: string;
10 | readonly stack: string[];
11 | } | null;
12 | readonly snapshot: Snapshot;
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/FilterType.ts:
--------------------------------------------------------------------------------
1 | const enum FilterType {
2 | ByCountry = 0,
3 | ByDecade = 1,
4 | ByGenre = 2,
5 | ByMood = 3,
6 | ByStyle = 4,
7 | ByAppleStationGenre = 101,
8 | ByPlexStationType = 102,
9 | }
10 |
11 | export default FilterType;
12 |
--------------------------------------------------------------------------------
/src/types/ItemType.ts:
--------------------------------------------------------------------------------
1 | const enum ItemType {
2 | Media = 0,
3 | Album = 1,
4 | Artist = 2,
5 | Playlist = 3,
6 | Folder = 4,
7 | }
8 |
9 | export default ItemType;
10 |
--------------------------------------------------------------------------------
/src/types/ItemsByService.d.ts:
--------------------------------------------------------------------------------
1 | import MediaItem from './MediaItem';
2 | import MediaService from './MediaService';
3 |
4 | export default interface ItemsByService {
5 | readonly service: MediaService;
6 | readonly items: readonly T[];
7 | }
8 |
--------------------------------------------------------------------------------
/src/types/LibraryAction.d.ts:
--------------------------------------------------------------------------------
1 | import Action from './Action';
2 |
3 | type LibraryAction = Extract<
4 | Action,
5 | Action.AddToLibrary | Action.RemoveFromLibrary | Action.Rate | Action.Like | Action.Unlike
6 | >;
7 |
8 | export default LibraryAction;
9 |
--------------------------------------------------------------------------------
/src/types/LinearType.ts:
--------------------------------------------------------------------------------
1 | const enum LinearType {
2 | NonLinear = 0,
3 | OnDemand = NonLinear,
4 | Station = 1, // Radio/TV station/channel
5 | Show = 10, // Radio/TV programme (no music metadata)
6 | MusicTrack = 20, // Music track (or music video)
7 | Ad = 99, // Commercial break
8 | OffAir = -1,
9 | }
10 |
11 | export default LinearType;
12 |
--------------------------------------------------------------------------------
/src/types/Listen.d.ts:
--------------------------------------------------------------------------------
1 | import MediaItem from './MediaItem';
2 |
3 | export interface ListenData {
4 | readonly sessionId: string;
5 | readonly lastfmScrobbledAt: number;
6 | readonly listenbrainzScrobbledAt: number;
7 | }
8 |
9 | type Listen = MediaItem & ListenData;
10 |
11 | export default Listen;
12 |
--------------------------------------------------------------------------------
/src/types/LookupStatus.ts:
--------------------------------------------------------------------------------
1 | const enum LookupStatus {
2 | Looking = 'looking',
3 | NotFound = 'not-found'
4 | }
5 |
6 | export default LookupStatus;
7 |
--------------------------------------------------------------------------------
/src/types/MediaAlbum.d.ts:
--------------------------------------------------------------------------------
1 | import BaseMediaObject from './BaseMediaObject';
2 | import ItemType from './ItemType';
3 | import MediaItem from './MediaItem';
4 | import Pager from './Pager';
5 |
6 | export default interface MediaAlbum extends BaseMediaObject {
7 | readonly itemType: ItemType.Album;
8 | readonly pager: Pager;
9 | // Everything below here should be optional.
10 | readonly artist?: string;
11 | readonly multiDisc?: boolean;
12 | readonly year?: number;
13 | readonly trackCount?: number;
14 | readonly duration?: number;
15 | readonly playedAt?: number; // UTC
16 | readonly unplayable?: boolean;
17 | readonly release_mbid?: string;
18 | readonly artist_mbids?: readonly string[];
19 | readonly caa_mbid?: string; // cover art archive
20 | readonly copyright?: string;
21 | readonly explicit?: boolean;
22 | readonly badge?: string;
23 | readonly shareLink?: string;
24 | }
25 |
--------------------------------------------------------------------------------
/src/types/MediaArtist.d.ts:
--------------------------------------------------------------------------------
1 | import BaseMediaObject from './BaseMediaObject';
2 | import ItemType from './ItemType';
3 | import MediaAlbum from './MediaAlbum';
4 | import Pager from './Pager';
5 |
6 | export default interface MediaArtist extends BaseMediaObject {
7 | readonly itemType: ItemType.Artist;
8 | readonly pager: Pager;
9 | // Everything below here should be optional.
10 | readonly artist_mbid?: string;
11 | readonly country?: string;
12 | }
13 |
--------------------------------------------------------------------------------
/src/types/MediaFilter.d.ts:
--------------------------------------------------------------------------------
1 | export default interface MediaFilter {
2 | readonly id: string;
3 | readonly title: string;
4 | readonly count?: number;
5 | }
6 |
--------------------------------------------------------------------------------
/src/types/MediaFolder.d.ts:
--------------------------------------------------------------------------------
1 | import BaseMediaObject from './BaseMediaObject';
2 | import ItemType from './ItemType';
3 | import MediaFolderItem from './MediaFolderItem';
4 | import Pager from './Pager';
5 |
6 | export default interface MediaFolder extends BaseMediaObject {
7 | readonly itemType: ItemType.Folder;
8 | readonly fileName: string;
9 | readonly path: string;
10 | readonly pager: Pager;
11 | // Everything below here should be optional.
12 | readonly parentFolder?: MediaFolder;
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/MediaFolderItem.d.ts:
--------------------------------------------------------------------------------
1 | import {SetRequired} from 'type-fest';
2 | import MediaFolder from './MediaFolder';
3 | import MediaItem from './MediaItem';
4 |
5 | type MediaFolderItem = MediaFolder | SetRequired;
6 |
7 | export default MediaFolderItem;
8 |
--------------------------------------------------------------------------------
/src/types/MediaObject.d.ts:
--------------------------------------------------------------------------------
1 | import MediaAlbum from './MediaAlbum';
2 | import MediaArtist from './MediaArtist';
3 | import MediaFolderItem from './MediaFolderItem';
4 | import MediaItem from './MediaItem';
5 | import MediaPlaylist from './MediaPlaylist';
6 |
7 | type MediaObject = MediaAlbum | MediaArtist | MediaItem | MediaPlaylist | MediaFolderItem;
8 |
9 | export default MediaObject;
10 |
--------------------------------------------------------------------------------
/src/types/MediaPlayback.d.ts:
--------------------------------------------------------------------------------
1 | import Player from './Player';
2 | import PlaylistItem from './PlaylistItem';
3 |
4 | export default interface MediaPlayback extends Player {
5 | stopAfterCurrent: boolean;
6 | eject(): void;
7 | loadAndPlay(item: PlaylistItem): void;
8 | next(): void;
9 | prev(): void;
10 | }
11 |
--------------------------------------------------------------------------------
/src/types/MediaPlaylist.d.ts:
--------------------------------------------------------------------------------
1 | import BaseMediaObject from './BaseMediaObject';
2 | import ItemType from './ItemType';
3 | import MediaItem from './MediaItem';
4 | import Pager from './Pager';
5 |
6 | export default interface MediaPlaylist extends BaseMediaObject {
7 | readonly itemType: ItemType.Playlist;
8 | readonly pager: Pager;
9 | // Everything below here should be optional
10 | readonly isOwn?: boolean;
11 | readonly owner?: {
12 | readonly name: string;
13 | readonly url?: string;
14 | };
15 | readonly trackCount?: number;
16 | readonly duration?: number;
17 | readonly playedAt?: number; // UTC
18 | readonly modifiedAt?: number; // UTC
19 | readonly isChart?: boolean;
20 | readonly unplayable?: boolean;
21 | }
22 |
--------------------------------------------------------------------------------
/src/types/MediaSearchParams.d.ts:
--------------------------------------------------------------------------------
1 | export default interface MediaSearchParams {
2 | readonly q?: string;
3 | readonly sortBy?: string;
4 | readonly sortOrder?: 1 | -1;
5 | }
6 |
--------------------------------------------------------------------------------
/src/types/MediaService.d.ts:
--------------------------------------------------------------------------------
1 | import DataService from './DataService';
2 | import PersonalMediaService from './PersonalMediaService';
3 | import PublicMediaService from './PublicMediaService';
4 |
5 | type MediaService =
6 | | PublicMediaService
7 | | PersonalMediaService
8 | | DataService;
9 |
10 | export default MediaService;
11 |
--------------------------------------------------------------------------------
/src/types/MediaServiceId.d.ts:
--------------------------------------------------------------------------------
1 | export type PublicMediaServiceId =
2 | | 'apple'
3 | | 'internet-radio'
4 | | 'mixcloud'
5 | | 'soundcloud'
6 | | 'spotify'
7 | | 'tidal'
8 | | 'youtube';
9 |
10 | export type PersonalMediaServiceId =
11 | | 'airsonic'
12 | | 'ampache'
13 | | 'emby'
14 | | 'gonic'
15 | | 'jellyfin'
16 | | 'navidrome'
17 | | 'plex'
18 | | 'subsonic';
19 |
20 | export type DataServiceId = 'lastfm' | 'listenbrainz';
21 |
22 | type MediaServiceId = PublicMediaServiceId | PersonalMediaServiceId | DataServiceId;
23 |
24 | export default MediaServiceId;
25 |
--------------------------------------------------------------------------------
/src/types/MediaType.ts:
--------------------------------------------------------------------------------
1 | const enum MediaType {
2 | Audio = 0,
3 | Video = 1,
4 | }
5 |
6 | export default MediaType;
7 |
--------------------------------------------------------------------------------
/src/types/MetadataChange.d.ts:
--------------------------------------------------------------------------------
1 | import MediaObject from './MediaObject';
2 |
3 | export default interface MetadataChange {
4 | readonly match: (object: T) => boolean;
5 | readonly values: Partial;
6 | }
7 |
--------------------------------------------------------------------------------
/src/types/NextVisualizerReason.d.ts:
--------------------------------------------------------------------------------
1 | type NextVisualizerReason =
2 | | 'new-media-item'
3 | | 'new-provider'
4 | | 'new-visualizers'
5 | | 'next-clicked'
6 | | 'transition'
7 | | 'error';
8 |
9 | export default NextVisualizerReason;
10 |
--------------------------------------------------------------------------------
/src/types/Pager.d.ts:
--------------------------------------------------------------------------------
1 | import type {Observable} from 'rxjs';
2 |
3 | export default interface Pager {
4 | readonly pageSize: number;
5 | readonly maxSize?: number | undefined;
6 | observeBusy: () => Observable;
7 | observeItems(): Observable;
8 | observeSize(): Observable;
9 | observeError(): Observable;
10 | fetchAt(index: number, length?: number): void;
11 | disconnect(): void;
12 | }
13 |
14 | export interface PagerConfig {
15 | readonly pageSize: number;
16 | readonly maxSize?: number;
17 | readonly lookup?: boolean; // lookup only (no background fetching)
18 | readonly noCache?: boolean; // disable caching (implementation specific)
19 | }
20 |
21 | export interface Page {
22 | readonly items: readonly T[];
23 | readonly total?: number;
24 | readonly atEnd?: boolean;
25 | }
26 |
--------------------------------------------------------------------------------
/src/types/ParentOf.d.ts:
--------------------------------------------------------------------------------
1 | import MediaAlbum from './MediaAlbum';
2 | import MediaArtist from './MediaArtist';
3 | import MediaFolder from './MediaFolder';
4 | import MediaItem from './MediaItem';
5 | import MediaObject from './MediaObject';
6 | import MediaPlaylist from './MediaPlaylist';
7 |
8 | type ParentOf = T extends MediaItem
9 | ? MediaAlbum | MediaPlaylist | MediaFolder
10 | : T extends MediaAlbum
11 | ? MediaArtist
12 | : T extends MediaFolder
13 | ? MediaFolder | undefined
14 | : never;
15 |
16 | export default ParentOf;
17 |
--------------------------------------------------------------------------------
/src/types/PersonalMediaLibrary.d.ts:
--------------------------------------------------------------------------------
1 | export default interface PersonalMediaLibrary {
2 | readonly id: string;
3 | readonly title: string;
4 | readonly type?: string;
5 | }
6 |
--------------------------------------------------------------------------------
/src/types/PersonalMediaServerSettings.d.ts:
--------------------------------------------------------------------------------
1 | import type {Observable} from 'rxjs';
2 | import PersonalMediaLibrary from './PersonalMediaLibrary';
3 |
4 | export default interface PersonalMediaServerSettings {
5 | readonly audioLibraries: readonly PersonalMediaLibrary[];
6 | readonly host: string;
7 | libraryId: string;
8 | libraries: readonly PersonalMediaLibrary[];
9 | readonly videoLibraryId?: string;
10 | observeLibraryId: () => Observable;
11 | useManualLogin?: boolean;
12 | userName?: string;
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/PersonalMediaService.d.ts:
--------------------------------------------------------------------------------
1 | import type React from 'react';
2 | import BaseMediaService from './BaseMediaService';
3 | import PersonalMediaLibrary from './PersonalMediaLibrary';
4 | import PersonalMediaServerSettings from './PersonalMediaServerSettings';
5 | import ServiceType from './ServiceType';
6 |
7 | export default interface PersonalMediaService
8 | extends BaseMediaService,
9 | Partial {
10 | readonly host: string;
11 | readonly serviceType: ServiceType.PersonalMedia;
12 | readonly Components?: BaseMediaService['Components'] & {
13 | ServerSettings?: React.FC<{service: PersonalMediaService}>;
14 | };
15 | // Everything below here should be optional.
16 | getLibraries?: () => Promise;
17 | getServerInfo?: () => Promise>;
18 | }
19 |
--------------------------------------------------------------------------------
/src/types/Pin.d.ts:
--------------------------------------------------------------------------------
1 | import {Except} from 'type-fest';
2 | import MediaAlbum from './MediaAlbum';
3 | import MediaArtist from './MediaArtist';
4 | import MediaFolder from './MediaFolder';
5 | import MediaPlaylist from './MediaPlaylist';
6 |
7 | export type Pinnable = MediaAlbum | MediaArtist | MediaPlaylist | MediaFolder;
8 |
9 | type Pin = Except & {
10 | readonly isPinned: true;
11 | };
12 |
13 | export default Pin;
14 |
--------------------------------------------------------------------------------
/src/types/PlayAction.d.ts:
--------------------------------------------------------------------------------
1 | import Action from './Action';
2 |
3 | type PlayAction = Extract;
4 |
5 | export default PlayAction;
6 |
--------------------------------------------------------------------------------
/src/types/PlayableItem.d.ts:
--------------------------------------------------------------------------------
1 | import MediaItem from './MediaItem';
2 |
3 | type PlayableItem = Pick<
4 | MediaItem,
5 | 'src' | 'srcs' | 'linearType' | 'playbackType' | 'isLivePlayback' | 'container' | 'startTime'
6 | > & {
7 | readonly duration?: number; // Make this optional
8 | };
9 |
10 | export default PlayableItem;
11 |
--------------------------------------------------------------------------------
/src/types/Playback.d.ts:
--------------------------------------------------------------------------------
1 | import type {Observable} from 'rxjs';
2 | import PlaylistItem from './PlaylistItem';
3 | import PlaybackState from './PlaybackState';
4 |
5 | export default interface Playback {
6 | paused: boolean;
7 | currentItem: PlaylistItem | null;
8 | currentTime: number;
9 | duration: number;
10 | observePlaybackState(): Observable;
11 | observePlaybackStart(): Observable;
12 | observePlaybackEnd(): Observable;
13 | observeCurrentItem(): Observable;
14 | observeCurrentTime(): Observable;
15 | observeDuration(): Observable;
16 | observePaused(): Observable;
17 | getPlaybackId(): string;
18 | getPlaybackState(): PlaybackState;
19 | ended(): void;
20 | pause(): void;
21 | play(): void;
22 | playing(): void;
23 | stop(): void;
24 | suspend(): void;
25 | unsuspend(): void;
26 | }
27 |
--------------------------------------------------------------------------------
/src/types/PlaybackState.d.ts:
--------------------------------------------------------------------------------
1 | import PlaylistItem from './PlaylistItem';
2 |
3 | export default interface PlaybackState {
4 | readonly currentItem: PlaylistItem | null;
5 | readonly currentTime: number;
6 | readonly startedAt: number; // Ms
7 | readonly endedAt: number; // Ms
8 | readonly duration: number;
9 | readonly paused: boolean;
10 | readonly playbackId: string;
11 | readonly miniPlayer: boolean; // State comes from miniPlayer.
12 | }
13 |
--------------------------------------------------------------------------------
/src/types/PlaybackType.ts:
--------------------------------------------------------------------------------
1 | const enum PlaybackType {
2 | Direct = 0,
3 | HLS = 1,
4 | DASH = 2, // Not currently used.
5 | IFrame = 3,
6 | Icecast = 4,
7 | IcecastM3u = 5, // `.m3u` or `.pls`
8 | IcecastOgg = 6,
9 | HLSMetadata = 11,
10 | }
11 |
12 | export default PlaybackType;
13 |
--------------------------------------------------------------------------------
/src/types/PlaylistItem.d.ts:
--------------------------------------------------------------------------------
1 | import LookupStatus from './LookupStatus';
2 | import MediaItem from './MediaItem';
3 | import UserData from './UserData';
4 |
5 | type PlaylistItem = Subtract & {
6 | readonly id: string;
7 | readonly lookupStatus?: LookupStatus;
8 | };
9 |
10 | export default PlaylistItem;
11 |
--------------------------------------------------------------------------------
/src/types/Preferences.d.ts:
--------------------------------------------------------------------------------
1 | export default interface Preferences {
2 | disableExplicitContent: boolean;
3 | markExplicitContent: boolean;
4 | mediaInfoTabs: boolean;
5 | miniPlayer: boolean;
6 | spacebarTogglePlay: boolean;
7 | }
--------------------------------------------------------------------------------
/src/types/PublicMediaService.d.ts:
--------------------------------------------------------------------------------
1 | import BaseMediaService from './BaseMediaService';
2 | import MediaType from './MediaType';
3 | import ServiceType from './ServiceType';
4 |
5 | export default interface PublicMediaService extends BaseMediaService {
6 | readonly serviceType: ServiceType.PublicMedia;
7 | readonly Components?: BaseMediaService['Components'] & {
8 | StreamingSettings?: React.FC<{service: PublicMediaService}>;
9 | };
10 | // Everything below here should be optional.
11 | readonly primaryMediaType?: MediaType;
12 | }
13 |
--------------------------------------------------------------------------------
/src/types/ReplayGainMode.d.ts:
--------------------------------------------------------------------------------
1 | type ReplayGainMode = '' | 'album' | 'track';
2 |
3 | export default ReplayGainMode;
4 |
--------------------------------------------------------------------------------
/src/types/ServiceType.ts:
--------------------------------------------------------------------------------
1 | const enum ServiceType {
2 | PublicMedia = 0,
3 | PersonalMedia = 1,
4 | DataService = 2,
5 | }
6 |
7 | export default ServiceType;
8 |
--------------------------------------------------------------------------------
/src/types/Theme.d.ts:
--------------------------------------------------------------------------------
1 | export default interface Theme {
2 | readonly name: string;
3 | readonly fontName?: string;
4 | readonly frameColor: string;
5 | readonly frameTextColor: string;
6 | readonly backgroundColor: string;
7 | readonly textColor: string;
8 | readonly selectedBackgroundColor: string;
9 | readonly selectedTextColor: string;
10 | readonly buttonColor?: string;
11 | readonly buttonTextColor?: string;
12 | readonly scrollbarColor?: string;
13 | readonly scrollbarTextColor?: string;
14 | readonly scrollbarThickness?: number;
15 | readonly mediaButtonColor?: string;
16 | readonly mediaButtonTextColor?: string;
17 | readonly roundness: number;
18 | readonly spacing: number;
19 | readonly flat: boolean;
20 | }
21 |
--------------------------------------------------------------------------------
/src/types/Thumbnail.d.ts:
--------------------------------------------------------------------------------
1 | export default interface Thumbnail {
2 | readonly url: string;
3 | readonly width: number;
4 | readonly height: number;
5 | }
6 |
--------------------------------------------------------------------------------
/src/types/UserData.d.ts:
--------------------------------------------------------------------------------
1 | export default interface UserData {
2 | readonly rating?: number;
3 | readonly globalLikes?: number;
4 | readonly globalRating?: number;
5 | readonly playCount?: number;
6 | readonly globalPlayCount?: number;
7 | readonly inLibrary?: boolean;
8 | readonly isPinned?: boolean;
9 | }
10 |
--------------------------------------------------------------------------------
/src/types/UserTheme.d.ts:
--------------------------------------------------------------------------------
1 | import Theme from './Theme';
2 |
3 | export default interface UserTheme extends Theme {
4 | readonly userTheme: true;
5 | }
6 |
--------------------------------------------------------------------------------
/src/types/VisualizerFavorite.d.ts:
--------------------------------------------------------------------------------
1 | import Visualizer from './Visualizer';
2 |
3 | type VisualizerFavorite = Pick;
4 |
5 | export default VisualizerFavorite;
6 |
--------------------------------------------------------------------------------
/src/types/VisualizerProvider.d.ts:
--------------------------------------------------------------------------------
1 | import type {Observable} from 'rxjs';
2 | import AudioManager from './AudioManager';
3 | import Player from './Player';
4 | import Visualizer from './Visualizer';
5 |
6 | export default interface VisualizerProvider {
7 | readonly id: T['providerId'];
8 | readonly name: string;
9 | readonly shortName?: string;
10 | readonly player?: Player;
11 | readonly visualizers: readonly T[];
12 | readonly externalUrl?: string;
13 | readonly defaultHidden?: boolean;
14 | createPlayer(audio: AudioManager): Player;
15 | observeVisualizers(): Observable;
16 | }
17 |
--------------------------------------------------------------------------------
/src/types/VisualizerProviderId.d.ts:
--------------------------------------------------------------------------------
1 | type VisualizerProviderId =
2 | | 'none'
3 | | 'ambientvideo'
4 | | 'ampshader'
5 | | 'audiomotion'
6 | | 'butterchurn'
7 | | 'coverart'
8 | | 'spotifyviz'
9 | | 'waveform'
10 | ;
11 |
12 | export default VisualizerProviderId;
13 |
--------------------------------------------------------------------------------
/src/utils/LiteStorage/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './LiteStorage';
2 | export * from './LiteStorage';
3 |
--------------------------------------------------------------------------------
/src/utils/LiteStorage/memoryStorage.ts:
--------------------------------------------------------------------------------
1 | const items = new Map();
2 |
3 | const memoryStorage: Storage = {
4 | get length(): number {
5 | return items.size;
6 | },
7 |
8 | clear(): void {
9 | items.clear();
10 | },
11 |
12 | key(index: number): string | null {
13 | return [...items.keys()][index] ?? null;
14 | },
15 |
16 | getItem(key: string): string | null {
17 | return items.get(String(key)) ?? null;
18 | },
19 |
20 | setItem(key: string, value: string): void {
21 | items.set(String(key), String(value));
22 | },
23 |
24 | removeItem(key: string): void {
25 | items.delete(String(key));
26 | },
27 | };
28 |
29 | export default memoryStorage;
30 |
--------------------------------------------------------------------------------
/src/utils/browser.ts:
--------------------------------------------------------------------------------
1 | import {detect} from 'detect-browser';
2 | import isElectron from 'is-electron';
3 |
4 | const browser = detect();
5 | const name = browser?.name || 'unknown';
6 | const displayName = /^edge/.test(name)
7 | ? 'Microsoft Edge'
8 | : name.replace(/^\w/, (char) => char.toUpperCase());
9 | const version = browser?.version || '0';
10 | const os = browser?.os || 'unknown';
11 | const cmdKey: keyof KeyboardEvent = os === 'Mac OS' ? 'metaKey' : 'ctrlKey';
12 | const cmdKeyStr = os === 'Mac OS' ? '⌘' : 'Ctrl';
13 | const desktop = !/^(iOS|Android OS|BlackBerry OS|Windows Mobile|Amazon OS)$/.test(os);
14 |
15 | export default {
16 | name,
17 | displayName,
18 | version,
19 | os,
20 | desktop,
21 | cmdKey,
22 | cmdKeyStr,
23 | isElectron: isElectron()
24 | };
25 |
--------------------------------------------------------------------------------
/src/utils/event.ts:
--------------------------------------------------------------------------------
1 | type AnyEvent = Pick;
2 |
3 | export function cancelEvent(event: AnyEvent): void {
4 | event.preventDefault();
5 | event.stopPropagation();
6 | }
7 |
8 | export function preventDefault(event: AnyEvent): void {
9 | event.preventDefault();
10 | }
11 |
12 | export function stopPropagation(event: AnyEvent): void {
13 | event.stopPropagation();
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './array';
2 | export * from './date';
3 | export * from './dom';
4 | export * from './event';
5 | export * from './fetch';
6 | export * from './media';
7 | export * from './number';
8 | export * from './string';
9 | export * from './utils';
10 | export {default as LiteStorage} from './LiteStorage';
11 | export {default as Logger} from './Logger';
12 | export {default as RateLimiter} from './RateLimiter';
13 | export {default as browser} from './browser';
14 |
--------------------------------------------------------------------------------
/src/utils/number.ts:
--------------------------------------------------------------------------------
1 | export function clamp(min: number, value: number, max: number): number {
2 | return Math.min(Math.max(value, min), max);
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | export function copyToClipboard(data: any): Promise {
2 | if (data && typeof data === 'object') {
3 | data = JSON.stringify(data, undefined, 2);
4 | }
5 | return navigator.clipboard.writeText(String(data));
6 | }
7 |
8 | export function exists(value: T): value is NonNullable {
9 | return value != null;
10 | }
11 |
12 | export function isFullscreenMedia(): boolean {
13 | return document.fullscreenElement?.id === 'media' || isMiniPlayer;
14 | }
15 |
16 | export const isMiniPlayer = !!(
17 | opener?.origin === location.origin &&
18 | window.name === sessionStorage.getItem('ampcast/session/miniPlayerId') &&
19 | location.hash === '#mini-player'
20 | );
21 |
22 | export function sleep(ms: number): Promise {
23 | return new Promise((resolve) => setTimeout(resolve, ms));
24 | }
25 |
--------------------------------------------------------------------------------