├── .husky └── pre-commit ├── pnpm-workspace.yaml ├── packages ├── ui │ ├── src │ │ ├── components │ │ │ ├── Box │ │ │ │ ├── index.ts │ │ │ │ └── Box.tsx │ │ │ ├── Tabs │ │ │ │ ├── index.ts │ │ │ │ ├── TabsList.tsx │ │ │ │ ├── context.ts │ │ │ │ ├── TabsPanels.tsx │ │ │ │ ├── TabsPanel.tsx │ │ │ │ └── TabsTab.tsx │ │ │ ├── Input │ │ │ │ ├── index.ts │ │ │ │ └── Input.test.tsx │ │ │ ├── Select │ │ │ │ ├── index.ts │ │ │ │ ├── SelectError.tsx │ │ │ │ ├── SelectLabel.tsx │ │ │ │ ├── SelectDescription.tsx │ │ │ │ ├── context.ts │ │ │ │ ├── SelectOption.tsx │ │ │ │ ├── SelectOptions.tsx │ │ │ │ ├── SelectButton.tsx │ │ │ │ └── Select.test.tsx │ │ │ ├── Toggle │ │ │ │ └── index.ts │ │ │ ├── Card │ │ │ │ ├── index.ts │ │ │ │ └── Card.test.tsx │ │ │ ├── PlayerBar │ │ │ │ ├── index.ts │ │ │ │ ├── PlayerBarRoot.tsx │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── PlayerBarSeekBar.test.tsx.snap │ │ │ │ ├── PlayerBarVolume.tsx │ │ │ │ ├── PlayerBar.tsx │ │ │ │ ├── PlayerBarNowPlaying.tsx │ │ │ │ ├── PlayerBarControls.tsx │ │ │ │ └── useSeekBar.ts │ │ │ ├── Toaster │ │ │ │ └── index.ts │ │ │ ├── ViewShell │ │ │ │ ├── index.ts │ │ │ │ └── ViewShell.tsx │ │ │ ├── Button │ │ │ │ └── index.tsx │ │ │ ├── Loader │ │ │ │ ├── index.ts │ │ │ │ └── Loader.tsx │ │ │ ├── CardGrid │ │ │ │ ├── index.ts │ │ │ │ ├── CardGrid.test.tsx │ │ │ │ └── CardGrid.tsx │ │ │ ├── Popover │ │ │ │ ├── index.ts │ │ │ │ └── Popover.test.tsx │ │ │ ├── SectionShell │ │ │ │ ├── index.tsx │ │ │ │ └── SectionShell.tsx │ │ │ ├── PluginItem │ │ │ │ └── index.ts │ │ │ ├── QueueItem │ │ │ │ ├── index.ts │ │ │ │ ├── QueueItem.tsx │ │ │ │ ├── types.ts │ │ │ │ └── variants.ts │ │ │ ├── QueuePanel │ │ │ │ ├── index.ts │ │ │ │ └── QueueReorderLayer.tsx │ │ │ ├── ThemeController │ │ │ │ ├── index.ts │ │ │ │ └── ThemeController.tsx │ │ │ ├── ImageReveal │ │ │ │ └── index.ts │ │ │ ├── PlayerWorkspace │ │ │ │ ├── index.ts │ │ │ │ ├── PlayerWorkspaceLeftSidebar.tsx │ │ │ │ ├── PlayerWorkspaceRightSidebar.tsx │ │ │ │ ├── constants.ts │ │ │ │ ├── PlayerWorkspace.test.tsx │ │ │ │ └── PlayerWorkspace.tsx │ │ │ ├── ScrollableArea │ │ │ │ └── index.ts │ │ │ ├── TrackTable │ │ │ │ ├── utils │ │ │ │ │ └── constants.ts │ │ │ │ ├── index.ts │ │ │ │ ├── Cells │ │ │ │ │ ├── PositionCell.tsx │ │ │ │ │ ├── TextCell.tsx │ │ │ │ │ ├── ThumbnailCell.tsx │ │ │ │ │ └── TitleCell.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── useSorting.ts │ │ │ │ │ ├── useReorder.ts │ │ │ │ │ ├── useGlobalFilter.ts │ │ │ │ │ └── useVirtualRows.ts │ │ │ │ ├── defaults.ts │ │ │ │ ├── ReorderLayer.tsx │ │ │ │ ├── TrackTableContext.tsx │ │ │ │ ├── FilterBar.tsx │ │ │ │ ├── labels.tsx │ │ │ │ ├── Headers │ │ │ │ │ ├── TextHeader.tsx │ │ │ │ │ └── IconHeader.tsx │ │ │ │ └── VirtualizedBody.tsx │ │ │ ├── Slider │ │ │ │ ├── index.ts │ │ │ │ ├── context.tsx │ │ │ │ └── useSliderWheel.ts │ │ │ ├── SidebarNavigation │ │ │ │ ├── index.ts │ │ │ │ ├── SidebarNavigationItem.tsx │ │ │ │ └── SidebarNavigation.tsx │ │ │ ├── BottomBar.tsx │ │ │ ├── PlayerShell.tsx │ │ │ ├── TopBar.tsx │ │ │ ├── index.ts │ │ │ └── PlayerShell.test.tsx │ │ ├── utils.ts │ │ ├── index.ts │ │ ├── resources │ │ │ ├── logotype.svg │ │ │ └── logo.svg │ │ ├── styles.css │ │ ├── test │ │ │ └── setup.ts │ │ └── utils │ │ │ └── time.ts │ ├── vite-env.d.ts │ └── tsconfig.json ├── player │ ├── vite.env.d.ts │ ├── src-tauri │ │ ├── build.rs │ │ ├── .gitignore │ │ ├── icons │ │ │ ├── icon.icns │ │ │ ├── icon.ico │ │ │ └── icon.png │ │ ├── src │ │ │ ├── main.rs │ │ │ └── lib.rs │ │ ├── capabilities │ │ │ └── desktop.json │ │ └── Cargo.toml │ ├── src │ │ ├── types │ │ │ └── assets.d.ts │ │ ├── routes │ │ │ ├── themes.tsx │ │ │ ├── dashboard.tsx │ │ │ ├── plugins.tsx │ │ │ ├── settings.tsx │ │ │ ├── index.tsx │ │ │ ├── search.tsx │ │ │ ├── album │ │ │ │ └── $providerId │ │ │ │ │ └── $albumId.tsx │ │ │ └── artist │ │ │ │ └── $providerId │ │ │ │ └── $artistId.tsx │ │ ├── services │ │ │ ├── tauri │ │ │ │ └── commands.ts │ │ │ ├── themeBootstrap.ts │ │ │ ├── themeService.ts │ │ │ ├── languageService.ts │ │ │ └── advancedThemeService.ts │ │ ├── test │ │ │ ├── utils │ │ │ │ ├── mockTrack.ts │ │ │ │ ├── seedPluginRegistry.ts │ │ │ │ ├── inMemoryTauriStore.ts │ │ │ │ └── testPluginFolder.ts │ │ │ ├── mocks │ │ │ │ └── plugin-dialog.ts │ │ │ ├── builders │ │ │ │ └── NuclearPluginBuilder.ts │ │ │ └── fixtures │ │ │ │ └── albums.ts │ │ ├── views │ │ │ ├── Dashboard.tsx │ │ │ ├── Album │ │ │ │ ├── hooks │ │ │ │ │ └── useAlbumDetails.ts │ │ │ │ └── Album.tsx │ │ │ ├── Artist │ │ │ │ ├── hooks │ │ │ │ │ ├── useArtistAlbums.ts │ │ │ │ │ ├── useArtistDetails.ts │ │ │ │ │ ├── useArtistRelatedArtists.ts │ │ │ │ │ └── useArtistTopTracks.ts │ │ │ │ ├── Artist.tsx │ │ │ │ ├── components │ │ │ │ │ └── ArtistPopularTracks.tsx │ │ │ │ └── Artist.test-wrapper.tsx │ │ │ ├── Plugins │ │ │ │ ├── Plugins.test.data.tsx │ │ │ │ └── Plugins.test-wrapper.tsx │ │ │ ├── Search │ │ │ │ ├── Search.test-wrapper.tsx │ │ │ │ └── Search.test.tsx │ │ │ └── Settings │ │ │ │ ├── NumberInputField.tsx │ │ │ │ ├── TextField.tsx │ │ │ │ ├── SelectField.tsx │ │ │ │ ├── Settings.test.tsx │ │ │ │ ├── SliderField.tsx │ │ │ │ ├── ToggleField.tsx │ │ │ │ ├── useSettingTranslation.ts │ │ │ │ ├── Settings.test-wrapper.tsx │ │ │ │ ├── useSettingsGroups.ts │ │ │ │ ├── SettingsSection.tsx │ │ │ │ └── Settings.tsx │ │ ├── hooks │ │ │ ├── useCurrentQueueItem.ts │ │ │ ├── useCoreSetting.ts │ │ │ ├── useQueue.ts │ │ │ └── useQueueActions.ts │ │ ├── stores │ │ │ ├── advancedThemeStore.ts │ │ │ ├── startupStore.ts │ │ │ └── soundStore.ts │ │ ├── components │ │ │ ├── ConnectedTrackTable.tsx │ │ │ ├── PluginIcon.tsx │ │ │ ├── DevTools.tsx │ │ │ ├── SoundProvider.tsx │ │ │ └── ConnectedQueuePanel.tsx │ │ ├── utils │ │ │ ├── path.ts │ │ │ └── logging.ts │ │ ├── App.tsx │ │ ├── App.test-wrapper.tsx │ │ ├── integration-tests │ │ │ └── Sound.test-wrapper.tsx │ │ └── main.tsx │ ├── tsconfig.node.json │ ├── index.html │ ├── tsconfig.json │ └── vite.config.ts ├── plugin-sdk │ ├── src │ │ ├── test │ │ │ └── setup.ts │ │ ├── api │ │ │ ├── api.test.ts │ │ │ ├── streaming.ts │ │ │ ├── providers.ts │ │ │ ├── settings.ts │ │ │ ├── index.ts │ │ │ └── metadata.ts │ │ ├── index.ts │ │ ├── types │ │ │ ├── providers.ts │ │ │ ├── metadata.ts │ │ │ ├── streaming.ts │ │ │ └── queue.ts │ │ ├── types.ts │ │ └── react │ │ │ └── useSetting.ts │ ├── tsconfig.json │ └── api-extractor.json ├── docs │ ├── package.json │ ├── themes │ │ ├── themes-basic.md │ │ └── themes.md │ ├── .gitbook.yaml │ ├── misc │ │ └── platform-specific.md │ ├── SUMMARY.md │ └── plugins │ │ ├── plugin-system.md │ │ └── providers.md ├── i18n │ ├── src │ │ ├── index.ts │ │ ├── types.ts │ │ └── i18n.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── package.json ├── tailwind-config │ ├── tsconfig.json │ └── package.json ├── themes │ ├── src │ │ ├── basic │ │ │ ├── index.ts │ │ │ ├── ember.css │ │ │ ├── aurora.css │ │ │ ├── canyon.css │ │ │ └── lagoon.css │ │ ├── advanced │ │ │ ├── __tests__ │ │ │ │ ├── schema.test.ts │ │ │ │ └── generator.test.ts │ │ │ ├── schema.ts │ │ │ └── generator.ts │ │ └── __tests__ │ │ │ └── runtime.test.ts │ ├── tsconfig.json │ ├── README.md │ └── vite.config.ts ├── hifi │ ├── src │ │ ├── index.ts │ │ ├── plugins │ │ │ ├── Volume.tsx │ │ │ ├── Stereo.tsx │ │ │ ├── BiQuadFilter.tsx │ │ │ └── Equalizer.tsx │ │ └── test │ │ │ └── setup.ts │ ├── tsconfig.json │ ├── package.json │ └── vite.config.ts ├── storybook │ ├── tsconfig.json │ ├── src │ │ ├── Loader.stories.tsx │ │ ├── Welcome.stories.tsx │ │ ├── Box.stories.tsx │ │ ├── ThemeController.stories.tsx │ │ ├── TopBar.stories.tsx │ │ ├── Popover.stories.tsx │ │ ├── CardGrid.stories.tsx │ │ └── Select.stories.tsx │ ├── .storybook │ │ ├── preview.ts │ │ └── main.ts │ └── package.json ├── eslint-config │ ├── tsconfig.json │ └── package.json └── model │ ├── tsconfig.json │ ├── src │ ├── queue.ts │ ├── search.ts │ └── streaming.ts │ ├── package.json │ └── vite.config.ts ├── .prettierignore ├── eslint.config.ts ├── .gitignore ├── crowdin.yml ├── prettier.config.js ├── tsconfig.json ├── .github └── workflows │ └── coverage.yml ├── turbo.json └── package.json /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | -------------------------------------------------------------------------------- /packages/ui/src/components/Box/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Box'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/Tabs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Tabs'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/Input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Input'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/Select/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Select'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/Toggle/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Toggle'; 2 | -------------------------------------------------------------------------------- /packages/player/vite.env.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /packages/plugin-sdk/src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/Card/index.ts: -------------------------------------------------------------------------------- 1 | export { Card } from './Card'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/PlayerBar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PlayerBar'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/Toaster/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Toaster'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/ViewShell/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ViewShell'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | export { Button } from './Button'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/Loader/index.ts: -------------------------------------------------------------------------------- 1 | export { Loader } from './Loader'; 2 | -------------------------------------------------------------------------------- /packages/ui/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/player/src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /packages/ui/src/components/CardGrid/index.ts: -------------------------------------------------------------------------------- 1 | export { CardGrid } from './CardGrid'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/Popover/index.ts: -------------------------------------------------------------------------------- 1 | export { Popover } from './Popover'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/SectionShell/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './SectionShell'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/PluginItem/index.ts: -------------------------------------------------------------------------------- 1 | export { PluginItem } from './PluginItem'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/QueueItem/index.ts: -------------------------------------------------------------------------------- 1 | export { QueueItem } from './QueueItem'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/QueuePanel/index.ts: -------------------------------------------------------------------------------- 1 | export { QueuePanel } from './QueuePanel'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/ThemeController/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ThemeController'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/ImageReveal/index.ts: -------------------------------------------------------------------------------- 1 | export { ImageReveal } from './ImageReveal'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/PlayerWorkspace/index.ts: -------------------------------------------------------------------------------- 1 | export { PlayerWorkspace } from './PlayerWorkspace'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/ScrollableArea/index.ts: -------------------------------------------------------------------------------- 1 | export { ScrollableArea } from './ScrollableArea'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | src-tauri/target/ 4 | src-tauri/Cargo.lock 5 | *.json 6 | *.md 7 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import config from './packages/eslint-config/eslint.config'; 2 | 3 | export default config; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | *.d.ts.map 5 | *.js.map 6 | .turbo 7 | storybook-static 8 | coverage -------------------------------------------------------------------------------- /packages/player/src/types/assets.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.wasm?url' { 2 | const url: string; 3 | export default url; 4 | } 5 | -------------------------------------------------------------------------------- /packages/player/src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /gen/schemas 5 | -------------------------------------------------------------------------------- /packages/ui/src/components/TrackTable/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_ROW_HEIGHT = 42; 2 | export const DEFAULT_OVERSCAN = 8; 3 | -------------------------------------------------------------------------------- /packages/player/src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuclearPlayer/nuclear-xrd/HEAD/packages/player/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /packages/player/src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuclearPlayer/nuclear-xrd/HEAD/packages/player/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /packages/player/src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuclearPlayer/nuclear-xrd/HEAD/packages/player/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /packages/ui/src/components/TrackTable/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TrackTable'; 2 | export * from './types'; 3 | export * from './labels'; 4 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuclearplayer/docs", 3 | "version": "0.0.9", 4 | "description": "Documentation", 5 | "type": "module" 6 | } -------------------------------------------------------------------------------- /packages/i18n/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as i18n } from './i18n'; 2 | export { useTranslation } from 'react-i18next'; 3 | export type { TFunction } from 'i18next'; 4 | -------------------------------------------------------------------------------- /packages/tailwind-config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Slider/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Slider, 3 | SliderHeader, 4 | SliderSurface, 5 | SliderTrack, 6 | SliderRangeInput, 7 | SliderFooter, 8 | } from './Slider'; 9 | -------------------------------------------------------------------------------- /packages/ui/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /packages/i18n/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { resources } from './i18n'; 2 | 3 | declare module 'i18next' { 4 | interface CustomTypeOptions { 5 | defaultNS: 'common'; 6 | resources: typeof resources; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | preserve_hierarchy: true 2 | 3 | files: 4 | - source: /packages/i18n/src/locales/en_US.json 5 | translation: /packages/i18n/src/locales/%locale_with_underscore%.json 6 | update_option: update_as_unapproved 7 | -------------------------------------------------------------------------------- /packages/player/src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | app_lib::run(); 6 | } 7 | -------------------------------------------------------------------------------- /packages/player/src/routes/themes.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | 3 | import { Themes } from '../views/Themes/Themes'; 4 | 5 | export const Route = createFileRoute('/themes')({ 6 | component: Themes, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/player/src/routes/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | 3 | import { Dashboard } from '../views/Dashboard'; 4 | 5 | export const Route = createFileRoute('/dashboard')({ 6 | component: Dashboard, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/player/src/routes/plugins.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | 3 | import { Plugins } from '../views/Plugins/Plugins'; 4 | 5 | export const Route = createFileRoute('/plugins')({ 6 | component: Plugins, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/player/src/routes/settings.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | 3 | import { Settings } from '../views/Settings/Settings'; 4 | 5 | export const Route = createFileRoute('/settings')({ 6 | component: Settings, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/SidebarNavigation/index.ts: -------------------------------------------------------------------------------- 1 | export { SidebarNavigation } from './SidebarNavigation'; 2 | export { SidebarNavigationCollapsible } from './SidebarNavigationCollapsible'; 3 | export { SidebarNavigationItem } from './SidebarNavigationItem'; 4 | -------------------------------------------------------------------------------- /packages/player/src/services/tauri/commands.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from '@tauri-apps/api/core'; 2 | 3 | export const copyDirRecursive = async ( 4 | from: string, 5 | to: string, 6 | ): Promise => { 7 | await invoke('copy_dir_recursive', { from, to }); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/themes/src/basic/index.ts: -------------------------------------------------------------------------------- 1 | export const BUILTIN_BASIC_THEME_IDS = [ 2 | 'nuclear:aurora', 3 | 'nuclear:ember', 4 | 'nuclear:lagoon', 5 | 'nuclear:canyon', 6 | ] as const; 7 | 8 | export type BuiltinBasicThemeId = (typeof BUILTIN_BASIC_THEME_IDS)[number]; 9 | -------------------------------------------------------------------------------- /packages/player/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute, redirect } from '@tanstack/react-router'; 2 | 3 | export const Route = createFileRoute('/')({ 4 | beforeLoad: () => { 5 | throw redirect({ 6 | to: '/dashboard', 7 | }); 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/player/src-tauri/capabilities/desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "desktop-capability", 3 | "platforms": [ 4 | "macOS", 5 | "windows", 6 | "linux" 7 | ], 8 | "windows": [ 9 | "main" 10 | ], 11 | "permissions": [ 12 | "updater:default" 13 | ] 14 | } -------------------------------------------------------------------------------- /packages/plugin-sdk/src/api/api.test.ts: -------------------------------------------------------------------------------- 1 | import { NuclearPluginAPI } from './index.js'; 2 | 3 | describe('NuclearPluginAPI', () => { 4 | it('should create an instance', () => { 5 | const api = new NuclearPluginAPI(); 6 | expect(api).toBeInstanceOf(NuclearPluginAPI); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/player/src/test/utils/mockTrack.ts: -------------------------------------------------------------------------------- 1 | import type { Track } from '@nuclearplayer/model'; 2 | 3 | export const createMockTrack = (title: string): Track => ({ 4 | title, 5 | artists: [{ name: 'Test Artist', roles: ['primary'] }], 6 | source: { provider: 'test', id: title.toLowerCase() }, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/player/src/views/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from '@nuclearplayer/i18n'; 2 | import { ViewShell } from '@nuclearplayer/ui'; 3 | 4 | export const Dashboard = () => { 5 | const { t } = useTranslation('dashboard'); 6 | return {t('content')}; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/player/src/hooks/useCurrentQueueItem.ts: -------------------------------------------------------------------------------- 1 | import type { QueueItem } from '@nuclearplayer/model'; 2 | 3 | import { useQueueStore } from '../stores/queue/queue.store'; 4 | 5 | export const useCurrentQueueItem = (): QueueItem | undefined => { 6 | return useQueueStore((state) => state.getCurrentItem()); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/themes/src/basic/ember.css: -------------------------------------------------------------------------------- 1 | :root[data-theme-id='nuclear:ember'] { 2 | --background: oklch(0.97 0.02 70); 3 | --primary: oklch(0.76 0.14 30); 4 | } 5 | 6 | :root[data-theme-id='nuclear:ember'][data-theme='dark'] { 7 | --background: oklch(0.4 0.03 277); 8 | --primary: oklch(0.76 0.14 30); 9 | } 10 | -------------------------------------------------------------------------------- /packages/themes/src/basic/aurora.css: -------------------------------------------------------------------------------- 1 | :root[data-theme-id='nuclear:aurora'] { 2 | --background: oklch(0.98 0.01 340); 3 | --primary: oklch(0.74 0.15 305); 4 | } 5 | 6 | :root[data-theme-id='nuclear:aurora'][data-theme='dark'] { 7 | --background: oklch(0.42 0.04 278); 8 | --primary: oklch(0.74 0.15 305); 9 | } 10 | -------------------------------------------------------------------------------- /packages/themes/src/basic/canyon.css: -------------------------------------------------------------------------------- 1 | :root[data-theme-id='nuclear:canyon'] { 2 | --background: oklch(0.975 0.02 70); 3 | --primary: oklch(0.68 0.19 38); 4 | } 5 | 6 | :root[data-theme-id='nuclear:canyon'][data-theme='dark'] { 7 | --background: oklch(0.36 0.03 30); 8 | --primary: oklch(0.68 0.19 38); 9 | } 10 | -------------------------------------------------------------------------------- /packages/themes/src/basic/lagoon.css: -------------------------------------------------------------------------------- 1 | :root[data-theme-id='nuclear:lagoon'] { 2 | --background: oklch(0.985 0.018 210); 3 | --primary: oklch(0.67 0.16 205); 4 | } 5 | 6 | :root[data-theme-id='nuclear:lagoon'][data-theme='dark'] { 7 | --background: oklch(0.33 0.035 245); 8 | --primary: oklch(0.67 0.16 205); 9 | } 10 | -------------------------------------------------------------------------------- /packages/ui/src/components/TrackTable/Cells/PositionCell.tsx: -------------------------------------------------------------------------------- 1 | import { CellContext } from '@tanstack/react-table'; 2 | 3 | import { Track } from '@nuclearplayer/model'; 4 | 5 | export const PositionCell = ({ 6 | getValue, 7 | }: CellContext) => { 8 | return {getValue()}; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/hifi/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Sound } from './Sound'; 2 | export { pluginFactory } from './pluginFactory'; 3 | export { Oscilloscope } from './Oscilloscope'; 4 | export { Volume } from './plugins/Volume'; 5 | export { Stereo } from './plugins/Stereo'; 6 | export { BiQuadFilter } from './plugins/BiQuadFilter'; 7 | export { Equalizer } from './plugins/Equalizer'; 8 | -------------------------------------------------------------------------------- /packages/player/src/hooks/useCoreSetting.ts: -------------------------------------------------------------------------------- 1 | import { useSetting } from '@nuclearplayer/plugin-sdk'; 2 | import type { SettingValue } from '@nuclearplayer/plugin-sdk'; 3 | 4 | import { coreSettingsHost } from '../services/settingsHost'; 5 | 6 | export const useCoreSetting = ( 7 | id: string, 8 | ) => useSetting(coreSettingsHost, id); 9 | -------------------------------------------------------------------------------- /packages/player/src/routes/search.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | import { z } from 'zod'; 3 | 4 | import { Search } from '../views/Search/Search'; 5 | 6 | export const Route = createFileRoute('/search')({ 7 | component: Search, 8 | validateSearch: z.object({ 9 | q: z.string().min(1).max(100).default(''), 10 | }), 11 | }); 12 | -------------------------------------------------------------------------------- /packages/player/src/test/mocks/plugin-dialog.ts: -------------------------------------------------------------------------------- 1 | import * as dialog from '@tauri-apps/plugin-dialog'; 2 | import { type Mock } from 'vitest'; 3 | 4 | vi.mock('@tauri-apps/plugin-dialog', () => ({ 5 | open: vi.fn(), 6 | })); 7 | 8 | export const PluginDialogMock = { 9 | setOpen: (value: string) => { 10 | (dialog.open as Mock).mockResolvedValue(value); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "moduleResolution": "bundler", 6 | "resolveJsonModule": true, 7 | "types": ["vite-plugin-svgr/client"] 8 | }, 9 | "include": ["src/**/*", ".storybook/**/*"], 10 | "exclude": ["node_modules", "storybook-static"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/docs/themes/themes-basic.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Use built-in presets. 3 | --- 4 | 5 | # Basic themes 6 | 7 | Basic themes are ready to go right out of the box. Head to Nuclear → Preferences → Themes and you'll see buttons for each preset (like Aurora and Ember). Click any button to change the player's look, or choose "Default" to clear all customizations and return to Nuclear's original style. -------------------------------------------------------------------------------- /packages/player/src/services/themeBootstrap.ts: -------------------------------------------------------------------------------- 1 | import { setThemeId } from '@nuclearplayer/themes'; 2 | 3 | import { useSettingsStore } from '../stores/settingsStore'; 4 | 5 | export const applyThemeFromSettings = async (): Promise => { 6 | const id = useSettingsStore.getState().getValue('core.theme.id'); 7 | if (typeof id === 'string' && id) { 8 | setThemeId(id); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ui/src/components/TrackTable/hooks/useSorting.ts: -------------------------------------------------------------------------------- 1 | import { SortingState } from '@tanstack/react-table'; 2 | import { useMemo, useState } from 'react'; 3 | 4 | export function useSorting() { 5 | const [sorting, setSorting] = useState([]); 6 | const isSorted = useMemo(() => sorting.length > 0, [sorting]); 7 | 8 | return { sorting, setSorting, isSorted } as const; 9 | } 10 | -------------------------------------------------------------------------------- /packages/player/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "noEmit": true, 10 | "types": ["vitest/globals", "vite-plugin-svgr/client"] 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/ui/src/components/TrackTable/Cells/TextCell.tsx: -------------------------------------------------------------------------------- 1 | import { CellContext } from '@tanstack/react-table'; 2 | 3 | import { Track } from '@nuclearplayer/model'; 4 | 5 | export const TextCell = ({ 6 | getValue, 7 | }: CellContext) => ( 8 | 9 |
{getValue()}
10 | 11 | ); 12 | -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | import '@nuclearplayer/tailwind-config'; 2 | import '@fontsource/dm-sans/400.css'; 3 | import '@fontsource/dm-sans/700.css'; 4 | import '@fontsource/bricolage-grotesque/800.css'; 5 | 6 | export * from './components'; 7 | export * from './utils'; 8 | 9 | export { setupResizeObserverMock } from './test/resizeObserverMock'; 10 | export { createFramerMotionMock } from './test/mockFramerMotion'; 11 | -------------------------------------------------------------------------------- /packages/ui/src/resources/logotype.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/eslint-config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": ".", 5 | "rootDir": "./src", 6 | "module": "CommonJS", 7 | "target": "ES2022", 8 | "moduleResolution": "node", 9 | "types": ["node"], 10 | "noEmit": false, 11 | "allowImportingTsExtensions": false 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/i18n/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src", 6 | "noEmit": false, 7 | "allowImportingTsExtensions": false, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "types": ["vitest/globals"] 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/model/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src", 6 | "noEmit": false, 7 | "allowImportingTsExtensions": false, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "types": ["vitest/globals"] 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/themes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "noEmit": false, 6 | "allowImportingTsExtensions": false, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "sourceMap": true, 10 | "types": ["vitest/globals", "@testing-library/jest-dom"] 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["node_modules", "dist"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/player/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Nuclear Player 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/docs/.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./ 2 | 3 | structure: 4 | readme: README.md 5 | summary: SUMMARY.md 6 | 7 | title: Nuclear Documentation 8 | description: Complete documentation for Nuclear music player 9 | 10 | plugins: 11 | - search 12 | - sharing 13 | - fontsettings 14 | - theme-default 15 | 16 | pdf: 17 | fontSize: 12 18 | paperSize: a4 19 | margin: 20 | top: 56 21 | bottom: 56 22 | left: 62 23 | right: 62 24 | -------------------------------------------------------------------------------- /packages/hifi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src", 6 | "noEmit": false, 7 | "allowImportingTsExtensions": false, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "types": ["vitest/globals", "@testing-library/jest-dom"] 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/model/src/queue.ts: -------------------------------------------------------------------------------- 1 | import type { Track } from './index'; 2 | 3 | export type QueueItem = { 4 | id: string; 5 | track: Track; 6 | status: 'idle' | 'loading' | 'success' | 'error'; 7 | error?: string; 8 | addedAtIso: string; 9 | }; 10 | 11 | export type RepeatMode = 'off' | 'all' | 'one'; 12 | 13 | export type Queue = { 14 | items: QueueItem[]; 15 | currentIndex: number; 16 | repeatMode: RepeatMode; 17 | shuffleEnabled: boolean; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/model/src/search.ts: -------------------------------------------------------------------------------- 1 | import type { AlbumRef, ArtistRef, PlaylistRef, Track } from './index'; 2 | 3 | export type SearchCategory = 'artists' | 'albums' | 'tracks' | 'playlists'; 4 | 5 | export type SearchParams = { 6 | query: string; 7 | types?: SearchCategory[]; 8 | limit?: number; 9 | }; 10 | 11 | export type SearchResults = { 12 | artists?: ArtistRef[]; 13 | albums?: AlbumRef[]; 14 | tracks?: Track[]; 15 | playlists?: PlaylistRef[]; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/ui/src/components/Select/SelectError.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { useSelectContext } from './context'; 4 | 5 | export const SelectError: FC<{ error?: string }> = ({ error }) => { 6 | const { 7 | ids: { errorId }, 8 | } = useSelectContext(); 9 | if (!error) { 10 | return null; 11 | } 12 | return ( 13 |

14 | {error} 15 |

16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/player/src/stores/advancedThemeStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | export type AdvancedThemeFile = { 4 | path: string; 5 | name: string; 6 | }; 7 | 8 | export type AdvancedThemeState = { 9 | themes: AdvancedThemeFile[]; 10 | setThemes: (themes: AdvancedThemeFile[]) => void; 11 | }; 12 | 13 | export const useAdvancedThemeStore = create((set) => ({ 14 | themes: [], 15 | setThemes: (themes) => set({ themes }), 16 | })); 17 | -------------------------------------------------------------------------------- /packages/ui/src/components/Card/Card.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | 3 | import { Card } from '.'; 4 | 5 | describe('Card', () => { 6 | it('(Snapshot) renders correctly', async () => { 7 | const { container } = render( 8 | , 13 | ); 14 | expect(container).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/ui/src/styles.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @layer utilities { 4 | .bg-stripes-diagonal { 5 | background-image: repeating-linear-gradient( 6 | 135deg, 7 | var(--primary) 0 8px, 8 | transparent 8px 16px 9 | ); 10 | animation: seekbar-stripes 1s linear infinite; 11 | background-size: 22.6px 22.6px; 12 | } 13 | 14 | @keyframes seekbar-stripes { 15 | to { 16 | background-position: 22.6px 0; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/docs/misc/platform-specific.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Platform specific considerations 3 | --- 4 | 5 | ## Paths 6 | 7 | ### Appdata 8 | 9 | - Linux: `~/.local/share/com.nuclearplayer/` 10 | - macOS: `~/Library/Application Support/com.nuclearplayer/` 11 | - Windows: `%APPDATA%/com.nuclearplayer/` 12 | 13 | ### Config 14 | 15 | - Linux: `~/.config/com.nuclearplayer/` 16 | - macOS: `~/Library/Application Support/com.nuclearplayer/config` 17 | - Windows: `%APPDATA%/com.nuclearplayer/config` 18 | -------------------------------------------------------------------------------- /packages/storybook/src/Loader.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryObj } from '@storybook/react-vite'; 2 | 3 | import { Loader } from '@nuclearplayer/ui'; 4 | 5 | const meta = { title: 'Components/Loader', component: Loader }; 6 | 7 | export default meta; 8 | type Story = StoryObj; 9 | 10 | export const Default: Story = { 11 | args: {}, 12 | }; 13 | 14 | export const LG: Story = { 15 | args: { size: 'lg' }, 16 | }; 17 | 18 | export const XL: Story = { 19 | args: { size: 'xl' }, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/ui/src/components/SidebarNavigation/SidebarNavigationItem.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | 3 | type SidebarNavigationItemProps = { 4 | children: ReactNode; 5 | isSelected?: boolean; 6 | isCollapsed?: boolean; 7 | }; 8 | export const SidebarNavigationItem: FC = ({ 9 | children, 10 | }) => { 11 | return ( 12 |
13 | {children} 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/ui/src/components/PlayerWorkspace/PlayerWorkspaceLeftSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { 4 | PlayerWorkspaceSidebar, 5 | PlayerWorkspaceSidebarPropsBase, 6 | } from './PlayerWorkspaceSidebar'; 7 | 8 | export const PlayerWorkspaceLeftSidebar: FC< 9 | PlayerWorkspaceSidebarPropsBase 10 | > = ({ children, ...props }) => { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/ui/src/components/PlayerWorkspace/PlayerWorkspaceRightSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { 4 | PlayerWorkspaceSidebar, 5 | PlayerWorkspaceSidebarPropsBase, 6 | } from './PlayerWorkspaceSidebar'; 7 | 8 | export const PlayerWorkspaceRightSidebar: FC< 9 | PlayerWorkspaceSidebarPropsBase 10 | > = ({ children, ...props }) => { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/plugin-sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export { NuclearPluginAPI, NuclearAPI } from './api'; 2 | export * from './types'; 3 | export * from './types/settings'; 4 | export * from './types/search'; 5 | export * from './types/queue'; 6 | export * from './types/streaming'; 7 | export * from './types/metadata'; 8 | export type { 9 | ProvidersHost, 10 | ProviderKind, 11 | ProviderDescriptor, 12 | } from './types/providers'; 13 | export { useSetting } from './react/useSetting'; 14 | export * from '@nuclearplayer/model'; 15 | -------------------------------------------------------------------------------- /packages/ui/src/components/TrackTable/Cells/ThumbnailCell.tsx: -------------------------------------------------------------------------------- 1 | import { CellContext } from '@tanstack/react-table'; 2 | 3 | import { Artwork, Track } from '@nuclearplayer/model'; 4 | 5 | export const ThumbnailCell = ({ 6 | getValue, 7 | }: CellContext) => { 8 | return ( 9 | 10 |
11 | 12 |
13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src", 6 | "noEmit": false, 7 | "allowImportingTsExtensions": false, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "types": [ 12 | "vitest/globals", 13 | "@testing-library/jest-dom", 14 | "vite-plugin-svgr/client" 15 | ] 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/ui/src/components/QueueItem/QueueItem.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { QueueItemCollapsed } from './QueueItemCollapsed'; 4 | import { QueueItemExpanded } from './QueueItemExpanded'; 5 | import type { QueueItemProps } from './types'; 6 | 7 | export const QueueItem: FC = ({ 8 | isCollapsed = false, 9 | ...props 10 | }) => { 11 | if (isCollapsed) { 12 | return ; 13 | } 14 | 15 | return ; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/ui/src/components/Select/SelectLabel.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { useSelectContext } from './context'; 4 | 5 | export const SelectLabel: FC<{ label?: string }> = ({ label }) => { 6 | const { 7 | ids: { labelId, selectId }, 8 | } = useSelectContext(); 9 | if (!label) { 10 | return null; 11 | } 12 | return ( 13 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/player/src/views/Album/hooks/useAlbumDetails.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import type { Album } from '@nuclearplayer/model'; 4 | 5 | import { metadataHost } from '../../../services/metadataHost'; 6 | 7 | export const useAlbumDetails = (providerId: string, albumId: string) => { 8 | return useQuery({ 9 | queryKey: ['album-details', providerId, albumId], 10 | queryFn: () => metadataHost.fetchAlbumDetails(albumId, providerId), 11 | enabled: Boolean(providerId && albumId), 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/plugin-sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "declarationDir": "./dist", 6 | "noEmit": false, 7 | "emitDeclarationOnly": true, 8 | "allowImportingTsExtensions": false, 9 | "declaration": true, 10 | "declarationMap": true, 11 | "sourceMap": false, 12 | "types": ["vitest/globals", "@testing-library/jest-dom"] 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/*.test.tsx"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/ui/src/components/BottomBar.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | 3 | import { cn } from '../utils'; 4 | 5 | type BottomBarProps = { 6 | children?: ReactNode; 7 | className?: string; 8 | }; 9 | 10 | export const BottomBar: FC = ({ children, className = '' }) => { 11 | return ( 12 |
18 | {children} 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | ## Plugins 4 | 5 | * [Getting started](plugins/getting-started.md) 6 | * [Plugin system](plugins/plugin-system.md) 7 | * [Settings](plugins/settings.md) 8 | * [Queue](plugins/queue.md) 9 | * [Streaming](plugins/streaming.md) 10 | * [Providers](plugins/providers.md) 11 | 12 | ## Theming 13 | 14 | * [Themes](themes/themes.md) 15 | * [Basic themes](themes/themes-basic.md) 16 | * [Advanced themes](themes/themes-advanced.md) 17 | 18 | ## Misc 19 | 20 | * [Platform-specific notes](misc/platform-specific.md) 21 | -------------------------------------------------------------------------------- /packages/player/src/views/Artist/hooks/useArtistAlbums.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import type { AlbumRef } from '@nuclearplayer/model'; 4 | 5 | import { metadataHost } from '../../../services/metadataHost'; 6 | 7 | export const useArtistAlbums = (providerId: string, artistId: string) => { 8 | return useQuery({ 9 | queryKey: ['artist-albums', providerId, artistId], 10 | queryFn: () => metadataHost.fetchArtistAlbums(artistId, providerId), 11 | enabled: Boolean(providerId && artistId), 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/player/src/views/Artist/hooks/useArtistDetails.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import type { Artist } from '@nuclearplayer/model'; 4 | 5 | import { metadataHost } from '../../../services/metadataHost'; 6 | 7 | export const useArtistDetails = (providerId: string, artistId: string) => { 8 | return useQuery({ 9 | queryKey: ['artist-details', providerId, artistId], 10 | queryFn: () => metadataHost.fetchArtistDetails(artistId, providerId), 11 | enabled: Boolean(providerId && artistId), 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/ui/src/components/PlayerShell.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | 3 | import { cn } from '../utils'; 4 | 5 | type PlayerShellProps = { 6 | children: ReactNode; 7 | className?: string; 8 | }; 9 | 10 | export const PlayerShell: FC = ({ 11 | children, 12 | className = '', 13 | }) => { 14 | return ( 15 |
21 | {children} 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/ui/src/components/Select/SelectDescription.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { useSelectContext } from './context'; 4 | 5 | export const SelectDescription: FC<{ description?: string }> = ({ 6 | description, 7 | }) => { 8 | const { 9 | ids: { descriptionId }, 10 | } = useSelectContext(); 11 | if (!description) { 12 | return null; 13 | } 14 | return ( 15 |

19 | {description} 20 |

21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/ui/src/components/CardGrid/CardGrid.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | 3 | import { CardGrid } from '.'; 4 | import { Card } from '..'; 5 | 6 | describe('Card grid', () => { 7 | it('(Snapshot) Renders correctly', () => { 8 | const { container } = render( 9 | 10 | 15 | , 16 | ); 17 | expect(container).toMatchSnapshot(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/hifi/src/plugins/Volume.tsx: -------------------------------------------------------------------------------- 1 | import { pluginFactory } from '../pluginFactory'; 2 | 3 | export type VolumeProps = { 4 | value: number; 5 | }; 6 | 7 | class VolumePlugin { 8 | createNode(ctx: AudioContext, props: VolumeProps): GainNode { 9 | const node = ctx.createGain(); 10 | node.gain.value = props.value / 100; 11 | return node; 12 | } 13 | updateNode(node: GainNode, props: VolumeProps): void { 14 | node.gain.value = props.value / 100; 15 | } 16 | } 17 | 18 | export const Volume = pluginFactory(new VolumePlugin()); 19 | -------------------------------------------------------------------------------- /packages/player/src/views/Plugins/Plugins.test.data.tsx: -------------------------------------------------------------------------------- 1 | export const fakePluginManifest = JSON.stringify({ 2 | name: 'nuclear-fake-plugin', 3 | version: '0.1.0', 4 | description: 'Fake plugin for testing', 5 | main: 'index.ts', 6 | license: 'AGPL-3.0-only', 7 | author: 'nukeop', 8 | keywords: ['nuclear', 'test'], 9 | nuclear: { 10 | displayName: 'Fake plugin', 11 | category: 'Robbing ships on the high seas', 12 | icon: { 13 | type: 'link', 14 | link: 'https://example.com/icon.png', 15 | }, 16 | permissions: [], 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/player/src/hooks/useQueue.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | import type { Queue } from '@nuclearplayer/model'; 4 | 5 | import { useQueueStore } from '../stores/queue/queue.store'; 6 | 7 | // You can't replace this with lodash pick because it causes infinite re-renders 8 | export const useQueue = (): Queue => { 9 | const { items, currentIndex, repeatMode, shuffleEnabled } = useQueueStore(); 10 | 11 | return useMemo( 12 | () => ({ items, currentIndex, repeatMode, shuffleEnabled }), 13 | [items, currentIndex, repeatMode, shuffleEnabled], 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/ui/src/components/TrackTable/defaults.ts: -------------------------------------------------------------------------------- 1 | import { TrackTableProps } from './types'; 2 | 3 | export const defaultFeatures: TrackTableProps['features'] = { 4 | header: true, 5 | filterable: true, 6 | sortable: true, 7 | reorderable: false, 8 | favorites: true, 9 | contextMenu: true, 10 | }; 11 | export const defaultDisplay: TrackTableProps['display'] = { 12 | displayDeleteButton: false, 13 | displayPosition: false, 14 | displayThumbnail: true, 15 | displayFavorite: true, 16 | displayArtist: true, 17 | displayDuration: true, 18 | displayQueueControls: true, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/hifi/src/plugins/Stereo.tsx: -------------------------------------------------------------------------------- 1 | import { pluginFactory } from '../pluginFactory'; 2 | 3 | export type StereoProps = { 4 | value: number; 5 | }; 6 | 7 | class StereoPlugin { 8 | createNode(ctx: AudioContext, props: StereoProps): StereoPannerNode { 9 | const node = ctx.createStereoPanner(); 10 | node.pan.value = props.value; 11 | return node; 12 | } 13 | updateNode(node: StereoPannerNode, props: StereoProps): void { 14 | node.pan.value = props.value; 15 | } 16 | } 17 | 18 | export const Stereo = pluginFactory( 19 | new StereoPlugin(), 20 | ); 21 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @import { Config } from "prettier" */ 2 | 3 | /** @type {Config} */ 4 | export default { 5 | semi: true, 6 | trailingComma: 'all', 7 | singleQuote: true, 8 | tabWidth: 2, 9 | importOrder: [ 10 | '', 11 | '', 12 | ' ', 13 | '^@nuclearplayer/(.*)$', 14 | ' ', 15 | '^[.]', 16 | '^[..]', 17 | ], 18 | importOrderParserPlugins: ['typescript', 'jsx'], 19 | plugins: [ 20 | '@ianvs/prettier-plugin-sort-imports', 21 | 'prettier-plugin-tailwindcss', 22 | ], 23 | tailwindFunctions: ['clsx', 'cn', 'cva'], 24 | }; 25 | -------------------------------------------------------------------------------- /packages/player/src/views/Artist/hooks/useArtistRelatedArtists.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import type { ArtistRef } from '@nuclearplayer/model'; 4 | 5 | import { metadataHost } from '../../../services/metadataHost'; 6 | 7 | export const useArtistRelatedArtists = ( 8 | providerId: string, 9 | artistId: string, 10 | ) => { 11 | return useQuery({ 12 | queryKey: ['artist-related-artists', providerId, artistId], 13 | queryFn: () => metadataHost.fetchArtistRelatedArtists(artistId, providerId), 14 | enabled: Boolean(providerId && artistId), 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/ui/src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | 3 | import { MotionGlobalConfig } from 'framer-motion'; 4 | 5 | import { setupResizeObserverMock } from './resizeObserverMock'; 6 | 7 | setupResizeObserverMock(); 8 | 9 | MotionGlobalConfig.skipAnimations = true; 10 | MotionGlobalConfig.instantAnimations = true; 11 | 12 | vi.mock('framer-motion', async (importOriginal) => { 13 | const mod = await importOriginal(); 14 | const mockMod = await import('./mockFramerMotion'); 15 | const factory = mockMod.createFramerMotionMock; 16 | return factory(mod); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/i18n/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | import dts from 'vite-plugin-dts'; 4 | 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: 'src/index.ts', 9 | name: 'i18n', 10 | fileName: (format) => (format === 'es' ? 'index.mjs' : 'index.cjs'), 11 | formats: ['es', 'cjs'], 12 | }, 13 | sourcemap: true, 14 | outDir: 'dist', 15 | emptyOutDir: true, 16 | }, 17 | plugins: [ 18 | react(), 19 | dts({ 20 | insertTypesEntry: true, 21 | outDir: 'dist', 22 | }), 23 | ], 24 | }); 25 | -------------------------------------------------------------------------------- /packages/player/src/routes/album/$providerId/$albumId.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | import z from 'zod'; 3 | 4 | import { Album } from '../../../views/Album/Album'; 5 | 6 | export const Route = createFileRoute('/album/$providerId/$albumId')({ 7 | params: { 8 | parse: (params) => ({ 9 | providerId: z.string().parse(params.providerId), 10 | albumId: z.string().parse(params.albumId), 11 | }), 12 | stringify: ({ providerId, albumId }) => ({ 13 | providerId: `${providerId}`, 14 | albumId: `${albumId}`, 15 | }), 16 | }, 17 | component: Album, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/player/src/views/Search/Search.test-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { render, RenderResult, screen } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | 4 | import App from '../../App'; 5 | 6 | export const SearchWrapper = { 7 | async mount(query?: string): Promise { 8 | const component = render(); 9 | 10 | const searchBox = await component.findByTestId('search-box'); 11 | userEvent.type(searchBox, query ?? 'test'); 12 | userEvent.type(searchBox, '{enter}'); 13 | await screen.findByTestId('search-view'); 14 | 15 | return component; 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/player/src/views/Settings/NumberInputField.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { Input } from '@nuclearplayer/ui'; 4 | 5 | type Props = { 6 | label: string; 7 | description?: string; 8 | value: number | string | undefined; 9 | setValue: (v: number) => void; 10 | }; 11 | 12 | export const NumberInputField: FC = ({ 13 | label, 14 | description, 15 | value, 16 | setValue, 17 | }) => ( 18 | setValue(Number(e.target.value))} 24 | /> 25 | ); 26 | -------------------------------------------------------------------------------- /packages/player/src/components/ConnectedTrackTable.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import type { Track } from '@nuclearplayer/model'; 4 | import { TrackTable, TrackTableProps } from '@nuclearplayer/ui'; 5 | 6 | import { useQueueActions } from '../hooks/useQueueActions'; 7 | 8 | export const ConnectedTrackTable: FC< 9 | Omit, 'actions'> 10 | > = (props) => { 11 | const queueActions = useQueueActions(); 12 | 13 | return ( 14 | queueActions.addToQueue([track]), 18 | }} 19 | /> 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/player/src/routes/artist/$providerId/$artistId.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | import z from 'zod'; 3 | 4 | import { Artist } from '../../../views/Artist/Artist'; 5 | 6 | export const Route = createFileRoute('/artist/$providerId/$artistId')({ 7 | params: { 8 | parse: (params) => ({ 9 | providerId: z.string().parse(params.providerId), 10 | artistId: z.string().parse(params.artistId), 11 | }), 12 | stringify: ({ providerId, artistId }) => ({ 13 | providerId: `${providerId}`, 14 | artistId: `${artistId}`, 15 | }), 16 | }, 17 | component: Artist, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/plugin-sdk/src/types/providers.ts: -------------------------------------------------------------------------------- 1 | export type ProviderKind = 'metadata' | 'streaming' | 'lyrics' | (string & {}); 2 | 3 | export type ProviderDescriptor = { 4 | id: string; 5 | kind: K; 6 | name: string; 7 | pluginId?: string; 8 | }; 9 | 10 | export type ProvidersHost = { 11 | register(provider: T): string; 12 | unregister(providerId: string): boolean; 13 | list( 14 | kind?: K, 15 | ): ProviderDescriptor[]; 16 | get(providerId: string): T | undefined; 17 | clear(): void; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/player/src/views/Settings/TextField.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { Input } from '@nuclearplayer/ui'; 4 | 5 | type Props = { 6 | label: string; 7 | description?: string; 8 | value: string | undefined; 9 | setValue: (v: string) => void; 10 | variant?: 'text' | 'password'; 11 | }; 12 | 13 | export const TextField: FC = ({ 14 | label, 15 | description, 16 | value, 17 | setValue, 18 | variant = 'text', 19 | }) => ( 20 | setValue(e.target.value)} 26 | /> 27 | ); 28 | -------------------------------------------------------------------------------- /packages/player/src/components/PluginIcon.tsx: -------------------------------------------------------------------------------- 1 | import { PluginIcon } from '@nuclearplayer/plugin-sdk'; 2 | 3 | interface PluginIconComponentProps { 4 | icon: PluginIcon | undefined; 5 | } 6 | 7 | export const PluginIconComponent = ({ icon }: PluginIconComponentProps) => { 8 | if (!icon || typeof icon !== 'object') { 9 | return null; 10 | } 11 | 12 | if (icon.type === 'link' && typeof icon.link === 'string') { 13 | return ( 14 | plugin icon 20 | ); 21 | } 22 | 23 | return null; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/tailwind-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuclearplayer/tailwind-config", 3 | "version": "0.0.9", 4 | "description": "Shared Tailwind CSS configuration for Nuclear", 5 | "type": "module", 6 | "main": "./global.css", 7 | "exports": { 8 | ".": "./global.css" 9 | }, 10 | "files": [ 11 | "global.css" 12 | ], 13 | "scripts": { 14 | "lint": "eslint .", 15 | "lint:fix": "eslint . --fix" 16 | }, 17 | "dependencies": { 18 | "@tailwindcss/vite": "^4.1.11", 19 | "tailwindcss": "^4.1.11", 20 | "tw-animate-css": "^1.3.6" 21 | }, 22 | "devDependencies": { 23 | "@nuclearplayer/eslint-config": "workspace:*" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/themes/src/advanced/__tests__/schema.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { AdvancedThemeSchema } from '../../advanced/schema'; 4 | 5 | describe('AdvancedThemeSchema', () => { 6 | it('accepts minimal valid theme', () => { 7 | const t = AdvancedThemeSchema.parse({ version: 1, name: 'Ok' }); 8 | expect(t).toEqual({ version: 1, name: 'Ok' }); 9 | }); 10 | 11 | it('rejects keys starting with --', () => { 12 | expect(() => 13 | AdvancedThemeSchema.parse({ 14 | version: 1, 15 | name: 'Bad', 16 | vars: { '--background': 'oklch(...)' }, 17 | }), 18 | ).toThrowError(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/ui/src/components/CardGrid/CardGrid.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | 3 | import { cn } from '../../utils'; 4 | 5 | type CardGridProps = { 6 | children?: ReactNode; 7 | className?: string; 8 | 'data-testid'?: string; 9 | }; 10 | 11 | export const CardGrid: FC = ({ 12 | children, 13 | className, 14 | 'data-testid': dataTestId, 15 | }) => { 16 | return ( 17 |
25 | {children} 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/player/src/components/DevTools.tsx: -------------------------------------------------------------------------------- 1 | import { TanStackDevtools } from '@tanstack/react-devtools'; 2 | import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'; 3 | import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'; 4 | import { FC } from 'react'; 5 | 6 | export const DevTools: FC = () => { 7 | return ( 8 | , 13 | }, 14 | { 15 | name: 'React Router', 16 | render: , 17 | }, 18 | ]} 19 | /> 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/player/src/views/Settings/SelectField.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { Select } from '@nuclearplayer/ui'; 4 | 5 | type Option = { id: string; label: string }; 6 | 7 | type Props = { 8 | label: string; 9 | description?: string; 10 | value: string | undefined; 11 | setValue: (v: string) => void; 12 | options: Option[]; 13 | }; 14 | 15 | export const SelectField: FC = ({ 16 | label, 17 | description, 18 | value, 19 | setValue, 20 | options, 21 | }) => ( 22 | onChange(e.target.value)} 19 | placeholder={placeholder ?? 'Filter tracks'} 20 | endAddon={ 21 |