diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..e1d4242e2 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,32 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install dependencies + run: npm ci + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run Playwright tests + run: npx playwright test + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 2e2ed0d14..f0f648c75 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,10 @@ src-tauri/gen .flatpak-builder release !src-tauri/release -.tanstack +.tanstack + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/package-lock.json b/package-lock.json index 3e79e2111..184a68da6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,9 +42,11 @@ "@lingui/babel-plugin-lingui-macro": "5.4.0", "@lingui/cli": "5.4.0", "@lingui/vite-plugin": "5.4.0", + "@playwright/test": "1.54.2", "@tanstack/router-plugin": "1.130.12", "@tauri-apps/cli": "2.7.1", "@types/lodash-es": "4.17.12", + "@types/node": "24.2.0", "@types/react": "19.1.9", "@types/react-dom": "19.1.7", "@types/semver": "7.7.0", @@ -1111,23 +1113,6 @@ "@types/istanbul-lib-coverage": "*" } }, - "node_modules/@lingui/babel-plugin-lingui-macro/node_modules/@lingui/conf/node_modules/jest-validate/node_modules/@jest/types/node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.8.0" - } - }, - "node_modules/@lingui/babel-plugin-lingui-macro/node_modules/@lingui/conf/node_modules/jest-validate/node_modules/@jest/types/node_modules/@types/node/node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "devOptional": true, - "license": "MIT" - }, "node_modules/@lingui/babel-plugin-lingui-macro/node_modules/@lingui/conf/node_modules/jest-validate/node_modules/@jest/types/node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -2177,23 +2162,6 @@ "@types/istanbul-lib-coverage": "*" } }, - "node_modules/@lingui/cli/node_modules/@lingui/conf/node_modules/jest-validate/node_modules/@jest/types/node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.8.0" - } - }, - "node_modules/@lingui/cli/node_modules/@lingui/conf/node_modules/jest-validate/node_modules/@jest/types/node_modules/@types/node/node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true, - "license": "MIT" - }, "node_modules/@lingui/cli/node_modules/@lingui/conf/node_modules/jest-validate/node_modules/@jest/types/node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -4707,23 +4675,6 @@ "@types/istanbul-lib-coverage": "*" } }, - "node_modules/@lingui/vite-plugin/node_modules/@lingui/conf/node_modules/jest-validate/node_modules/@jest/types/node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.8.0" - } - }, - "node_modules/@lingui/vite-plugin/node_modules/@lingui/conf/node_modules/jest-validate/node_modules/@jest/types/node_modules/@types/node/node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true, - "license": "MIT" - }, "node_modules/@lingui/vite-plugin/node_modules/@lingui/conf/node_modules/jest-validate/node_modules/@jest/types/node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -4922,6 +4873,69 @@ "dev": true, "license": "ISC" }, + "node_modules/@playwright/test": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz", + "integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.54.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz", + "integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.54.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@playwright/test/node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/@playwright/test/node_modules/playwright/node_modules/playwright-core": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz", + "integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@tanstack/react-query": { "version": "5.84.1", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.1.tgz", @@ -7644,6 +7658,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", diff --git a/package.json b/package.json index af17c4a40..1659a8ce5 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "gen:translations": "lingui extract --clean --overwrite", "test:typings": "tsc --noEmit --project ./tsconfig.json", "test:unit": "vitest run", + "test:ui": "playwright test", "test:lint": "biome ci .", "test:lint:fix": "biome check --write .", "package:checksums": "bash scripts/checksum.sh", @@ -66,9 +67,11 @@ "@lingui/babel-plugin-lingui-macro": "5.4.0", "@lingui/cli": "5.4.0", "@lingui/vite-plugin": "5.4.0", + "@playwright/test": "1.54.2", "@tanstack/router-plugin": "1.130.12", "@tauri-apps/cli": "2.7.1", "@types/lodash-es": "4.17.12", + "@types/node": "24.2.0", "@types/react": "19.1.9", "@types/react-dom": "19.1.7", "@types/semver": "7.7.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..90ea58b1a --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,33 @@ +import process from 'node:process'; + +import { defineConfig, devices } from '@playwright/test'; + +/** + * Playwright Config https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: process.env.CI !== undefined, + retries: process.env.CI ? 2 : 0, // retries? in our moment of triumph? + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? 'github' : 'list', + + use: { + baseURL: 'http://localhost:1420', + trace: 'on-first-retry', + }, + + projects: [ + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: 1420, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/src/components/GlobalKeyBindings.tsx b/src/components/GlobalKeyBindings.tsx index b669e9b9a..b029c9fac 100644 --- a/src/components/GlobalKeyBindings.tsx +++ b/src/components/GlobalKeyBindings.tsx @@ -1,7 +1,7 @@ -import { invoke } from '@tauri-apps/api/core'; import { useCallback } from 'react'; import Keybinding from 'react-keybinding-component'; +import SettingsBridge from '../lib/bridge-settings'; import player from '../lib/player'; import { usePlayerAPI } from '../stores/usePlayerStore'; @@ -32,7 +32,7 @@ function GlobalKeyBindings() { playerAPI.jumpTo(player.getCurrentTime() + 10); break; case 'Alt': - await invoke('plugin:app-menu|toggle'); + await SettingsBridge.toggleMenu(); break; default: break; diff --git a/src/components/PlayerEvents.tsx b/src/components/PlayerEvents.tsx index 39cf35e0d..c167cef7a 100644 --- a/src/components/PlayerEvents.tsx +++ b/src/components/PlayerEvents.tsx @@ -3,7 +3,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window'; import { sendNotification } from '@tauri-apps/plugin-notification'; import { useEffect } from 'react'; -import config from '../lib/config'; +import ConfigBridge from '../lib/bridge-config'; import { getCover } from '../lib/cover'; import player from '../lib/player'; import { goToPlayingTrack } from '../lib/queue-origin'; @@ -59,7 +59,7 @@ function PlayerEvents() { async function notifyTrackChange() { const track = player.getTrack(); - const isEnabled = await config.get('notifications'); + const isEnabled = await ConfigBridge.get('notifications'); const isMinimized = await getCurrentWindow() .isMinimized() .catch(logAndNotifyError); @@ -90,7 +90,10 @@ function PlayerEvents() { // If follow track is enabled, switch to the right view + scroll to the track // Do not do it if the app if focused, as users could be interacting with the app - if ((await config.get('audio_follow_playing_track')) && !isFocused) { + if ( + (await ConfigBridge.get('audio_follow_playing_track')) && + !isFocused + ) { goToPlayingTrack(queueOrigin, navigate); } } diff --git a/src/lib/config.ts b/src/lib/bridge-config.ts similarity index 83% rename from src/lib/config.ts rename to src/lib/bridge-config.ts index dde96d80a..44e1913e8 100644 --- a/src/lib/config.ts +++ b/src/lib/bridge-config.ts @@ -4,11 +4,10 @@ import type { Config } from '../generated/typings'; import { logAndNotifyError } from './utils'; /** - * Config Bridge for the UI to communicate with the backend + * Config Bridge for the UI to communicate with the backend. + * Grouped here so they're easier to mock in E2E tests. */ -class ConfigManager { - initialConfig: Config | null = null; - +const ConfigBridge = { /** * Get the initial value of the config at the time of instantiation. * Should only be used when starting the app. @@ -19,17 +18,16 @@ class ConfigManager { } return window.__MUSEEKS_INITIAL_CONFIG[key]; - } + }, async getAll(): Promise { - // TODO: check data shape? return invoke('plugin:config|get_config'); - } + }, async get(key: T): Promise { const config = await this.getAll(); return config[key]; - } + }, async set(key: T, value: Config[T]): Promise { const config = await this.getAll(); @@ -42,7 +40,7 @@ class ConfigManager { } return; - } -} + }, +}; -export default new ConfigManager(); +export default ConfigBridge; diff --git a/src/lib/database.ts b/src/lib/bridge-database.ts similarity index 98% rename from src/lib/database.ts rename to src/lib/bridge-database.ts index fb44f5c6a..5abe419ab 100644 --- a/src/lib/database.ts +++ b/src/lib/bridge-database.ts @@ -9,9 +9,9 @@ import type { } from '../generated/typings'; /** - * Bridge for the UI to communicate with the backend and manipulate the Database + * Bridge for the UI to communicate with the backend and manipulate the Database. + * Grouped here so they're easier to mock in E2E tests. */ - // biome-ignore lint/complexity/noStaticOnlyClass: conflict with decorators rules class DatabaseBridge { // --------------------------------------------------------------------------- diff --git a/src/lib/bridge-settings.ts b/src/lib/bridge-settings.ts new file mode 100644 index 000000000..2faaab8e0 --- /dev/null +++ b/src/lib/bridge-settings.ts @@ -0,0 +1,36 @@ +import { invoke } from '@tauri-apps/api/core'; + +/** + * Bridge for various Settings-related actions. + * Grouped here so they're easier to mock in E2E tests. + */ +const SettingsBridge = { + /** + * Show/hide global menu + */ + async toggleMenu(): Promise { + return invoke('plugin:app-menu|toggle'); + }, + + /** + * Set default view on startup + */ + async setDefaultView(defaultView: string): Promise { + return invoke('plugin:default-view|set', { + defaultView, + }); + }, + + /** + * Toggle sleep blocker + */ + async toggleSleepBlocker(value: boolean): Promise { + if (value === true) { + await invoke('plugin:sleepblocker|enable'); + } else { + await invoke('plugin:sleepblocker|disable'); + } + }, +}; + +export default SettingsBridge; diff --git a/src/lib/player.ts b/src/lib/player.ts index 347d9c2aa..5bfb3df5a 100644 --- a/src/lib/player.ts +++ b/src/lib/player.ts @@ -1,7 +1,7 @@ import { convertFileSrc } from '@tauri-apps/api/core'; import type { Track } from '../generated/typings'; -import config from './config'; +import ConfigBridge from './bridge-config'; import { logAndNotifyError } from './utils'; interface PlayerOptions { @@ -133,8 +133,8 @@ class Player { */ export default new Player({ - volume: config.getInitial('audio_volume'), - playbackRate: config.getInitial('audio_playback_rate') ?? 1, - audioOutputDevice: config.getInitial('audio_output_device'), - muted: config.getInitial('audio_muted'), + volume: ConfigBridge.getInitial('audio_volume'), + playbackRate: ConfigBridge.getInitial('audio_playback_rate') ?? 1, + audioOutputDevice: ConfigBridge.getInitial('audio_output_device'), + muted: ConfigBridge.getInitial('audio_muted'), }); diff --git a/src/routes/artists.$artistID.tsx b/src/routes/artists.$artistID.tsx index b85c83800..2f0c974f5 100644 --- a/src/routes/artists.$artistID.tsx +++ b/src/routes/artists.$artistID.tsx @@ -4,17 +4,17 @@ import { useMemo } from 'react'; import TrackList from '../components/TrackList'; import { useFilteredTrackGroup } from '../hooks/useFilteredTracks'; import useGlobalTrackListStatus from '../hooks/useGlobalTrackListStatus'; -import config from '../lib/config'; -import database from '../lib/database'; +import ConfigBridge from '../lib/bridge-config'; +import DatabaseBridge from '../lib/bridge-database'; import type { QueueOrigin } from '../types/museeks'; export const Route = createFileRoute('/artists/$artistID')({ component: ViewArtistDetails, loader: async ({ params }) => { const [albums, playlists, tracksDensity] = await Promise.all([ - database.getArtistTracks(params.artistID), - database.getAllPlaylists(), - config.get('track_view_density'), + DatabaseBridge.getArtistTracks(params.artistID), + DatabaseBridge.getAllPlaylists(), + ConfigBridge.get('track_view_density'), ]); return { diff --git a/src/routes/artists.tsx b/src/routes/artists.tsx index 1a6f183c2..42cf1769d 100644 --- a/src/routes/artists.tsx +++ b/src/routes/artists.tsx @@ -10,13 +10,13 @@ import SideNav from '../components/SideNav'; import SideNavLink from '../components/SideNavLink'; import View from '../elements/View'; import * as ViewMessage from '../elements/ViewMessage'; -import database from '../lib/database'; +import DatabaseBridge from '../lib/bridge-database'; import usePlayerStore from '../stores/usePlayerStore'; export const Route = createFileRoute('/artists')({ component: ViewArtists, beforeLoad: async ({ params }) => { - const artists = await database.getAllArtists(); + const artists = await DatabaseBridge.getAllArtists(); const queueOrigin = usePlayerStore.getState().queueOrigin; if (!('artistID' in params) && artists.length > 0) { diff --git a/src/routes/library.tsx b/src/routes/library.tsx index 6fa907f50..ed0afbddc 100644 --- a/src/routes/library.tsx +++ b/src/routes/library.tsx @@ -8,8 +8,8 @@ import View from '../elements/View'; import * as ViewMessage from '../elements/ViewMessage'; import useFilteredTracks from '../hooks/useFilteredTracks'; import useGlobalTrackListStatus from '../hooks/useGlobalTrackListStatus'; -import config from '../lib/config'; -import database from '../lib/database'; +import ConfigBridge from '../lib/bridge-config'; +import DatabaseBridge from '../lib/bridge-database'; import queryClient from '../lib/query-client'; import useLibraryStore from '../stores/useLibraryStore'; import type { QueueOrigin } from '../types/museeks'; @@ -22,12 +22,12 @@ export const Route = createFileRoute('/library')({ loader: async () => { queryClient.prefetchQuery({ queryKey: [QUERY_ALL_TRACKS], - queryFn: database.getAllTracks, + queryFn: DatabaseBridge.getAllTracks, }); const [playlists, tracksDensity] = await Promise.all([ - database.getAllPlaylists(), - config.get('track_view_density'), + DatabaseBridge.getAllPlaylists(), + ConfigBridge.get('track_view_density'), ]); return { @@ -50,7 +50,7 @@ function ViewLibrary() { // Using stale-while-revalidate libraries help us (fake-)loading this page faster const { data: tracks, isLoading } = useQuery({ queryKey: [QUERY_ALL_TRACKS], - queryFn: database.getAllTracks, + queryFn: DatabaseBridge.getAllTracks, refetchOnWindowFocus: false, refetchOnReconnect: false, }); diff --git a/src/routes/playlists.$playlistID.tsx b/src/routes/playlists.$playlistID.tsx index ab1ca21ed..598e6df59 100644 --- a/src/routes/playlists.$playlistID.tsx +++ b/src/routes/playlists.$playlistID.tsx @@ -13,8 +13,8 @@ import type { Track } from '../generated/typings'; import useFilteredTracks from '../hooks/useFilteredTracks'; import useGlobalTrackListStatus from '../hooks/useGlobalTrackListStatus'; import useInvalidate from '../hooks/useInvalidate'; -import config from '../lib/config'; -import database from '../lib/database'; +import ConfigBridge from '../lib/bridge-config'; +import DatabaseBridge from '../lib/bridge-database'; import PlaylistsAPI from '../stores/PlaylistsAPI'; import useLibraryStore from '../stores/useLibraryStore'; import type { QueueOrigin } from '../types/museeks'; @@ -23,11 +23,11 @@ export const Route = createFileRoute('/playlists/$playlistID')({ component: ViewPlaylistDetails, loader: async ({ params }) => { try { - const playlist = await database.getPlaylist(params.playlistID); + const playlist = await DatabaseBridge.getPlaylist(params.playlistID); const [playlistTracks, tracksDensity] = await Promise.all([ - database.getTracks(playlist.tracks), - config.get('track_view_density'), + DatabaseBridge.getTracks(playlist.tracks), + ConfigBridge.get('track_view_density'), ]); return { diff --git a/src/routes/playlists.tsx b/src/routes/playlists.tsx index b8434e97c..65614c8fc 100644 --- a/src/routes/playlists.tsx +++ b/src/routes/playlists.tsx @@ -18,14 +18,14 @@ import ButtonIcon from '../elements/ButtonIcon'; import View from '../elements/View'; import * as ViewMessage from '../elements/ViewMessage'; import useInvalidate from '../hooks/useInvalidate'; -import database from '../lib/database'; +import DatabaseBridge from '../lib/bridge-database'; import PlaylistsAPI from '../stores/PlaylistsAPI'; import usePlayerStore from '../stores/usePlayerStore'; export const Route = createFileRoute('/playlists')({ component: ViewPlaylists, beforeLoad: async ({ params }) => { - const playlists = await database.getAllPlaylists(); + const playlists = await DatabaseBridge.getAllPlaylists(); const queueOrigin = usePlayerStore.getState().queueOrigin; if (!('playlistID' in params) && playlists.length > 0) { @@ -108,7 +108,7 @@ function ViewPlaylists() { { text: t`Export`, action: async () => { - await database.exportPlaylist(playlist.id); + await DatabaseBridge.exportPlaylist(playlist.id); }, }, ]; diff --git a/src/routes/settings.tsx b/src/routes/settings.tsx index dab4a026c..f1d437c40 100644 --- a/src/routes/settings.tsx +++ b/src/routes/settings.tsx @@ -5,7 +5,7 @@ import { invoke } from '@tauri-apps/api/core'; import { SettingsNav, SettingsNavLink } from '../elements/SettingsNav'; import View from '../elements/View'; -import config from '../lib/config'; +import ConfigBridge from '../lib/bridge-config'; import styles from './settings.module.css'; export const Route = createFileRoute('/settings')({ @@ -18,7 +18,7 @@ export const Route = createFileRoute('/settings')({ async loader() { const [configContent, version, tauriVersion, appStorageDir] = await Promise.all([ - config.getAll(), + ConfigBridge.getAll(), getVersion(), getTauriVersion(), invoke('plugin:config|get_storage_dir'), diff --git a/src/routes/settings.ui.tsx b/src/routes/settings.ui.tsx index 7f3401712..bbc61354b 100644 --- a/src/routes/settings.ui.tsx +++ b/src/routes/settings.ui.tsx @@ -9,6 +9,7 @@ import CheckboxSetting from '../components/SettingCheckbox'; import Button from '../elements/Button'; import type { Config, DefaultView } from '../generated/typings'; import useInvalidate, { useInvalidateCallback } from '../hooks/useInvalidate'; +import SettingsBridge from '../lib/bridge-settings'; import { themes } from '../lib/themes'; import SettingsAPI, { DEFAULT_MAIN_COLOR } from '../stores/SettingsAPI'; import { ALL_LANGUAGES } from '../translations/languages'; @@ -114,7 +115,7 @@ function ViewSettingsUI() { value={config.default_view} description={t`Change the default view when starting the application`} onChange={(e) => - SettingsAPI.setDefaultView( + SettingsBridge.setDefaultView( e.currentTarget.value as DefaultView, ).then(invalidate) } @@ -142,7 +143,7 @@ function ViewSettingsUI() { title={t`Sleep mode blocker`} description={t`Prevent the computer from going into sleep mode when playing`} value={config.sleepblocker} - onChange={useInvalidateCallback(SettingsAPI.toggleSleepBlocker)} + onChange={useInvalidateCallback(SettingsBridge.toggleSleepBlocker)} /> diff --git a/src/routes/tracks.$trackID.tsx b/src/routes/tracks.$trackID.tsx index 607214544..5d08ddd89 100644 --- a/src/routes/tracks.$trackID.tsx +++ b/src/routes/tracks.$trackID.tsx @@ -10,7 +10,7 @@ import Separator from '../elements/Separator'; import View from '../elements/View'; import { parseDuration } from '../hooks/useFormattedDuration'; import useInvalidate from '../hooks/useInvalidate'; -import database from '../lib/database'; +import DatabaseBridge from '../lib/bridge-database'; import { useLibraryAPI } from '../stores/useLibraryStore'; import type { TrackMutation } from '../types/museeks'; import styles from './tracks.$trackID.module.css'; @@ -27,7 +27,7 @@ export const Route = createFileRoute('/tracks/$trackID')({ throw new Error('Track ID should not be null'); } - const [track] = await database.getTracks([trackID]); + const [track] = await DatabaseBridge.getTracks([trackID]); if (track == null) { throw new Error('Track not found'); diff --git a/src/stores/PlaylistsAPI.ts b/src/stores/PlaylistsAPI.ts index 8a7198a26..14659a513 100644 --- a/src/stores/PlaylistsAPI.ts +++ b/src/stores/PlaylistsAPI.ts @@ -1,7 +1,7 @@ import { t } from '@lingui/core/macro'; import type { Playlist, Track } from '../generated/typings'; -import database from '../lib/database'; +import DatabaseBridge from '../lib/bridge-database'; import { logAndNotifyError } from '../lib/utils'; import usePlayerStore from './usePlayerStore'; import useToastsStore from './useToastsStore'; @@ -11,8 +11,8 @@ import useToastsStore from './useToastsStore'; */ async function play(playlistID: string): Promise { try { - const playlist = await database.getPlaylist(playlistID); - const tracks = await database.getTracks(playlist.tracks); + const playlist = await DatabaseBridge.getPlaylist(playlistID); + const tracks = await DatabaseBridge.getTracks(playlist.tracks); usePlayerStore .getState() .api.start(tracks, null, { type: 'playlist', playlistID }); @@ -30,7 +30,7 @@ async function create( silent = false, ): Promise { try { - const playlist = await database.createPlaylist(name, trackIDs); + const playlist = await DatabaseBridge.createPlaylist(name, trackIDs); if (!silent) { useToastsStore @@ -51,7 +51,7 @@ async function create( */ async function rename(playlistID: string, name: string): Promise { try { - await database.renamePlaylist(playlistID, name); + await DatabaseBridge.renamePlaylist(playlistID, name); } catch (err) { logAndNotifyError(err); } @@ -62,7 +62,7 @@ async function rename(playlistID: string, name: string): Promise { */ async function remove(playlistID: string): Promise { try { - await database.deletePlaylist(playlistID); + await DatabaseBridge.deletePlaylist(playlistID); } catch (err) { logAndNotifyError(err); } @@ -76,9 +76,9 @@ async function addTracks( tracksIDs: string[], ): Promise { try { - const playlist = await database.getPlaylist(playlistID); + const playlist = await DatabaseBridge.getPlaylist(playlistID); const playlistTracks = playlist.tracks.concat(tracksIDs); - await database.setPlaylistTracks(playlistID, playlistTracks); + await DatabaseBridge.setPlaylistTracks(playlistID, playlistTracks); } catch (err) { logAndNotifyError(err); } @@ -92,11 +92,11 @@ async function removeTracks( tracksIDs: string[], ): Promise { try { - const playlist = await database.getPlaylist(playlistID); + const playlist = await DatabaseBridge.getPlaylist(playlistID); const playlistTracks = playlist.tracks.filter( (elem: string) => !tracksIDs.includes(elem), ); - await database.setPlaylistTracks(playlistID, playlistTracks); + await DatabaseBridge.setPlaylistTracks(playlistID, playlistTracks); } catch (err) { logAndNotifyError(err); } @@ -107,8 +107,11 @@ async function removeTracks( */ async function duplicate(playlistID: string): Promise { try { - const playlist = await database.getPlaylist(playlistID); - await database.createPlaylist(`Copy of ${playlist.name}`, playlist.tracks); + const playlist = await DatabaseBridge.getPlaylist(playlistID); + await DatabaseBridge.createPlaylist( + `Copy of ${playlist.name}`, + playlist.tracks, + ); } catch (err) { logAndNotifyError(err); } @@ -125,7 +128,7 @@ async function reorderTracks( ): Promise { try { // Save and reload the playlist - await database.setPlaylistTracks( + await DatabaseBridge.setPlaylistTracks( playlistID, tracks.map((track) => track.id), ); diff --git a/src/stores/SettingsAPI.ts b/src/stores/SettingsAPI.ts index ce9e61402..0f5e36eff 100644 --- a/src/stores/SettingsAPI.ts +++ b/src/stores/SettingsAPI.ts @@ -1,12 +1,11 @@ import { t } from '@lingui/core/macro'; import { getVersion } from '@tauri-apps/api/app'; -import { invoke } from '@tauri-apps/api/core'; import { getCurrentWindow } from '@tauri-apps/api/window'; import { info } from '@tauri-apps/plugin-log'; import * as semver from 'semver'; -import type { Config, DefaultView } from '../generated/typings'; -import config from '../lib/config'; +import type { Config } from '../generated/typings'; +import ConfigBridge from '../lib/bridge-config'; import { loadTranslation } from '../lib/i18n'; import { getTheme } from '../lib/themes'; import { logAndNotifyError } from '../lib/utils'; @@ -34,9 +33,9 @@ async function init(then: () => void): Promise { getCurrentWindow() .theme() .then((maybeTheme) => maybeTheme ?? 'light'), - config - .get('ui_accent_color') - .then((maybeColor) => maybeColor ?? DEFAULT_MAIN_COLOR), + ConfigBridge.get('ui_accent_color').then( + (maybeColor) => maybeColor ?? DEFAULT_MAIN_COLOR, + ), checkForUpdate({ silentFail: true }), ]); @@ -66,11 +65,11 @@ async function init(then: () => void): Promise { const setLanguage = async (language: Config['language']): Promise => { await loadTranslation(language); - await config.set('language', language); + await ConfigBridge.set('language', language); }; const setTheme = async (themeID: string): Promise => { - await config.set('theme', themeID); + await ConfigBridge.set('theme', themeID); switch (themeID) { case '__system': { @@ -109,13 +108,13 @@ function applyThemeToUI(themeID: string): void { async function setTracksDensity( density: Config['track_view_density'], ): Promise { - await config.set('track_view_density', density); + await ConfigBridge.set('track_view_density', density); } const setUIMainColor = async ( mainColor: Config['ui_accent_color'], ): Promise => { - await config.set('ui_accent_color', mainColor); + await ConfigBridge.set('ui_accent_color', mainColor); }; const applyUIMainColorToUI = (mainColor: Config['ui_accent_color']) => { @@ -134,7 +133,7 @@ const applyUIMainColorToUI = (mainColor: Config['ui_accent_color']) => { async function checkForUpdate( options: { silentFail?: boolean } = {}, ): Promise { - const shouldCheck = await config.get('auto_update_checker'); + const shouldCheck = await ConfigBridge.get('auto_update_checker'); if (!shouldCheck) { return; @@ -186,35 +185,15 @@ async function checkForUpdate( } } -/** - * Toggle sleep blocker - */ -async function toggleSleepBlocker(value: boolean): Promise { - if (value === true) { - await invoke('plugin:sleepblocker|enable'); - } else { - await invoke('plugin:sleepblocker|disable'); - } -} - -/** - * Set the default view of the app - */ -async function setDefaultView(defaultView: DefaultView): Promise { - await invoke('plugin:default-view|set', { - defaultView, - }); -} - /** * Toggle library refresh on startup */ async function toggleLibraryAutorefresh(value: boolean): Promise { - await config.set('library_autorefresh', value); + await ConfigBridge.set('library_autorefresh', value); } async function checkForLibraryRefresh(): Promise { - const autorefreshEnabled = config.getInitial('library_autorefresh'); + const autorefreshEnabled = ConfigBridge.getInitial('library_autorefresh'); if (autorefreshEnabled) { useLibraryStore.getState().api.scan(); @@ -225,21 +204,21 @@ async function checkForLibraryRefresh(): Promise { * Toggle update check on startup */ async function toggleAutoUpdateChecker(value: boolean): Promise { - await config.set('auto_update_checker', value); + await ConfigBridge.set('auto_update_checker', value); } /** * Toggle native notifications display */ async function toggleDisplayNotifications(value: boolean): Promise { - await config.set('notifications', value); + await ConfigBridge.set('notifications', value); } /** * Toggle follow track on track change */ async function toggleFollowPlayingTrack(value: boolean): Promise { - await config.set('audio_follow_playing_track', value); + await ConfigBridge.set('audio_follow_playing_track', value); } // Should we use something else to harmonize between zustand and non-store APIs? @@ -252,8 +231,6 @@ const SettingsAPI = { applyUIMainColorToUI, setTracksDensity, checkForUpdate, - toggleSleepBlocker, - setDefaultView, toggleLibraryAutorefresh, toggleAutoUpdateChecker, toggleDisplayNotifications, diff --git a/src/stores/useLibraryStore.ts b/src/stores/useLibraryStore.ts index 505ce5335..acf48a2fc 100644 --- a/src/stores/useLibraryStore.ts +++ b/src/stores/useLibraryStore.ts @@ -3,8 +3,8 @@ import type { StateCreator } from 'zustand'; import { persist } from 'zustand/middleware'; import type { SortBy, SortOrder } from '../generated/typings'; -import config from '../lib/config'; -import database from '../lib/database'; +import ConfigBridge from '../lib/bridge-config'; +import DatabaseBridge from '../lib/bridge-database'; import { logAndNotifyError } from '../lib/utils'; import { removeRedundantFolders } from '../lib/utils-library'; import type { API, TrackListStatusInfo, TrackMutation } from '../types/museeks'; @@ -41,8 +41,8 @@ type LibraryState = API<{ const useLibraryStore = createLibraryStore((set, get) => ({ search: '', - sortBy: config.getInitial('library_sort_by'), - sortOrder: config.getInitial('library_sort_order'), + sortBy: ConfigBridge.getInitial('library_sort_by'), + sortOrder: ConfigBridge.getInitial('library_sort_order'), refreshing: false, refresh: { current: 0, @@ -77,8 +77,8 @@ const useLibraryStore = createLibraryStore((set, get) => ({ sortOrder = 'Asc'; } - await config.set('library_sort_by', sortBy); - await config.set('library_sort_order', sortOrder); + await ConfigBridge.set('library_sort_by', sortBy); + await ConfigBridge.set('library_sort_order', sortOrder); set({ sortBy, sortOrder }); }, @@ -90,8 +90,11 @@ const useLibraryStore = createLibraryStore((set, get) => ({ try { set({ refreshing: true }); - const libraryFolders = await config.get('library_folders'); - const scanResult = await database.importTracks(libraryFolders, refresh); + const libraryFolders = await ConfigBridge.get('library_folders'); + const scanResult = await DatabaseBridge.importTracks( + libraryFolders, + refresh, + ); if (scanResult.track_count > 0) { const message = refresh @@ -122,22 +125,22 @@ const useLibraryStore = createLibraryStore((set, get) => ({ addLibraryFolders: async (paths: Array) => { try { - const musicFolders = await config.get('library_folders'); + const musicFolders = await ConfigBridge.get('library_folders'); const newFolders = removeRedundantFolders([ ...musicFolders, ...paths, ]).sort(); - await config.set('library_folders', newFolders); + await ConfigBridge.set('library_folders', newFolders); } catch (err) { logAndNotifyError(err); } }, removeLibraryFolder: async (path) => { - const musicFolders = await config.get('library_folders'); + const musicFolders = await ConfigBridge.get('library_folders'); const index = musicFolders.indexOf(path); musicFolders.splice(index, 1); - await config.set('library_folders', musicFolders); + await ConfigBridge.set('library_folders', musicFolders); }, setRefresh: async (current, total) => { @@ -153,7 +156,7 @@ const useLibraryStore = createLibraryStore((set, get) => ({ * remove tracks from library */ remove: async (tracksIDs) => { - await database.removeTracks(tracksIDs); + await DatabaseBridge.removeTracks(tracksIDs); // That would be great to remove those ids from all the playlists, but it's not easy // and should not cause strange behaviors, all PR for that would be really appreciated // TODO: see if it's possible to remove the IDs from the selected state of TrackList as it "could" lead to strange behaviors @@ -165,8 +168,8 @@ const useLibraryStore = createLibraryStore((set, get) => ({ reset: async () => { usePlayerStore.getState().api.stop(); try { - await database.reset(); - await config.set('library_folders', []); + await DatabaseBridge.reset(); + await ConfigBridge.set('library_folders', []); useToastsStore.getState().api.add('success', t`Library was reset`); } catch (err) { logAndNotifyError(err); @@ -183,7 +186,7 @@ const useLibraryStore = createLibraryStore((set, get) => ({ */ updateTrackMetadata: async (trackID, newFields) => { try { - let [track] = await database.getTracks([trackID]); + let [track] = await DatabaseBridge.getTracks([trackID]); if (!track) { throw new Error( @@ -196,7 +199,7 @@ const useLibraryStore = createLibraryStore((set, get) => ({ ...newFields, }; - await database.updateTrack(track); + await DatabaseBridge.updateTrack(track); } catch (err) { logAndNotifyError( err, diff --git a/src/stores/usePlayerStore.ts b/src/stores/usePlayerStore.ts index a17573b03..8aba6056d 100644 --- a/src/stores/usePlayerStore.ts +++ b/src/stores/usePlayerStore.ts @@ -3,8 +3,8 @@ import type { StateCreator } from 'zustand'; import { persist } from 'zustand/middleware'; import type { Repeat, Track } from '../generated/typings'; -import config from '../lib/config'; -import database from '../lib/database'; +import ConfigBridge from '../lib/bridge-config'; +import DatabaseBridge from '../lib/bridge-database'; import player from '../lib/player'; import { logAndNotifyError } from '../lib/utils'; import { shuffleTracks } from '../lib/utils-player'; @@ -52,8 +52,8 @@ const usePlayerStore = createPlayerStore((set, get) => ({ oldQueue: [], // Queue backup (in case of shuffle) queueCursor: null, // The cursor of the queue queueOrigin: null, // URL of the queue when it was started - repeat: config.getInitial('audio_repeat'), // the current repeat state (one, all, none) - shuffle: config.getInitial('audio_shuffle'), // If shuffle mode is enabled + repeat: ConfigBridge.getInitial('audio_repeat'), // the current repeat state (one, all, none) + shuffle: ConfigBridge.getInitial('audio_shuffle'), // If shuffle mode is enabled playerStatus: PlayerStatus.STOP, // Player status api: { @@ -233,7 +233,7 @@ const usePlayerStore = createPlayerStore((set, get) => ({ */ toggleShuffle: async (shuffle) => { const nextShuffleState: boolean = shuffle ?? !get().shuffle; - await config.set('audio_shuffle', nextShuffleState); + await ConfigBridge.set('audio_shuffle', nextShuffleState); const { queue, queueCursor, oldQueue } = get(); @@ -288,7 +288,7 @@ const usePlayerStore = createPlayerStore((set, get) => ({ } } - await config.set('audio_repeat', nextRepeatState); + await ConfigBridge.set('audio_repeat', nextRepeatState); set({ repeat: nextRepeatState }); }, @@ -307,7 +307,7 @@ const usePlayerStore = createPlayerStore((set, get) => ({ if (muted) player.mute(); else player.unmute(); - await config.set('audio_muted', muted); + await ConfigBridge.set('audio_muted', muted); }, /** @@ -316,10 +316,10 @@ const usePlayerStore = createPlayerStore((set, get) => ({ setPlaybackRate: async (value) => { // if in allowed range if (!Number.isNaN(value) && value >= 0.5 && value <= 5) { - await config.set('audio_playback_rate', value); + await ConfigBridge.set('audio_playback_rate', value); player.setPlaybackRate(value); } else { - await config.set('audio_playback_rate', null); + await ConfigBridge.set('audio_playback_rate', null); player.setPlaybackRate(1.0); } }, @@ -331,7 +331,7 @@ const usePlayerStore = createPlayerStore((set, get) => ({ if (deviceID) { try { await player.setOutputDevice(deviceID); - await config.set('audio_output_device', deviceID); + await ConfigBridge.set('audio_output_device', deviceID); } catch (err) { logAndNotifyError(err); } @@ -401,7 +401,7 @@ const usePlayerStore = createPlayerStore((set, get) => ({ */ addInQueue: async (tracksIDs) => { const { queue, queueCursor } = get(); - const tracks = await database.getTracks(tracksIDs); + const tracks = await DatabaseBridge.getTracks(tracksIDs); const newQueue = [...queue, ...tracks]; set({ @@ -415,7 +415,7 @@ const usePlayerStore = createPlayerStore((set, get) => ({ * Add tracks at the beginning of the queue */ addNextInQueue: async (tracksIDs) => { - const tracks = await database.getTracks(tracksIDs); + const tracks = await DatabaseBridge.getTracks(tracksIDs); const { queueCursor } = get(); const queue = [...get().queue]; @@ -546,5 +546,5 @@ function createPlayerStore(store: StateCreator) { * Make sure we don't save audio volume to the file system too often */ const saveVolume = debounce(async (volume: number) => { - await config.set('audio_volume', volume); + await ConfigBridge.set('audio_volume', volume); }, 500); diff --git a/src/translations/en.po b/src/translations/en.po index 7cbf6c7a7..83da3512b 100644 --- a/src/translations/en.po +++ b/src/translations/en.po @@ -66,7 +66,7 @@ msgstr "Artists" #: src/components/Footer.tsx:39 #: src/routes/playlists.tsx:169 -#: src/routes/settings.ui.tsx:126 +#: src/routes/settings.ui.tsx:127 msgid "Playlists" msgstr "Playlists" @@ -212,8 +212,8 @@ msgstr "Cancel" msgid "Remove" msgstr "Remove" -#. placeholder {0}: props.count #. placeholder {0}: tracks.length +#. placeholder {0}: props.count #: src/components/TrackListGrouped.tsx:102 #: src/components/TrackListStatus.tsx:11 msgid "{0, plural, one {# track} other {# tracks}}" @@ -449,7 +449,7 @@ msgid "Reset library?" msgstr "Reset library?" #: src/routes/settings.library.tsx:143 -#: src/routes/settings.ui.tsx:65 +#: src/routes/settings.ui.tsx:66 msgid "Reset" msgstr "Reset" @@ -469,75 +469,75 @@ msgstr "Interface" msgid "About" msgstr "About" -#: src/routes/settings.ui.tsx:36 +#: src/routes/settings.ui.tsx:37 msgid "Theme" msgstr "Theme" -#: src/routes/settings.ui.tsx:37 +#: src/routes/settings.ui.tsx:38 msgid "Change the appearance of the interface" msgstr "Change the appearance of the interface" -#: src/routes/settings.ui.tsx:43 +#: src/routes/settings.ui.tsx:44 msgid "System (default)" msgstr "System (default)" -#: src/routes/settings.ui.tsx:55 +#: src/routes/settings.ui.tsx:56 msgid "Accent color" msgstr "Accent color" -#: src/routes/settings.ui.tsx:76 +#: src/routes/settings.ui.tsx:77 msgid "Language" msgstr "Language" -#: src/routes/settings.ui.tsx:94 +#: src/routes/settings.ui.tsx:95 msgid "Tracks density" msgstr "Tracks density" -#: src/routes/settings.ui.tsx:95 +#: src/routes/settings.ui.tsx:96 msgid "Change the tracks spacing" msgstr "Change the tracks spacing" -#: src/routes/settings.ui.tsx:104 +#: src/routes/settings.ui.tsx:105 msgid "Normal (default)" msgstr "Normal (default)" -#: src/routes/settings.ui.tsx:107 +#: src/routes/settings.ui.tsx:108 msgid "Compact" msgstr "Compact" -#: src/routes/settings.ui.tsx:113 +#: src/routes/settings.ui.tsx:114 msgid "Default view" msgstr "Default view" -#: src/routes/settings.ui.tsx:115 +#: src/routes/settings.ui.tsx:116 msgid "Change the default view when starting the application" msgstr "Change the default view when starting the application" -#: src/routes/settings.ui.tsx:123 +#: src/routes/settings.ui.tsx:124 msgid "Library (default)" msgstr "Library (default)" -#: src/routes/settings.ui.tsx:132 +#: src/routes/settings.ui.tsx:133 msgid "Display Notifications" msgstr "Display Notifications" -#: src/routes/settings.ui.tsx:133 +#: src/routes/settings.ui.tsx:134 msgid "Send notifications when the playing track changes" msgstr "Send notifications when the playing track changes" -#: src/routes/settings.ui.tsx:142 +#: src/routes/settings.ui.tsx:143 msgid "Sleep mode blocker" msgstr "Sleep mode blocker" -#: src/routes/settings.ui.tsx:143 +#: src/routes/settings.ui.tsx:144 msgid "Prevent the computer from going into sleep mode when playing" msgstr "Prevent the computer from going into sleep mode when playing" -#: src/routes/settings.ui.tsx:158 +#: src/routes/settings.ui.tsx:159 msgid "Light" msgstr "Light" -#: src/routes/settings.ui.tsx:160 +#: src/routes/settings.ui.tsx:161 msgid "Dark" msgstr "Dark" @@ -596,29 +596,29 @@ msgid "The playlist \"{name}\" was created" msgstr "The playlist \"{name}\" was created" #. placeholder {0}: newRelease.tag_name -#: src/stores/SettingsAPI.ts:171 +#: src/stores/SettingsAPI.ts:170 msgid "Museeks {0} is available, check https://museeks.io!" msgstr "Museeks {0} is available, check https://museeks.io!" -#: src/stores/SettingsAPI.ts:173 +#: src/stores/SettingsAPI.ts:172 msgid "Museeks {currentVersion} is the latest version available." msgstr "Museeks {currentVersion} is the latest version available." #. placeholder {0}: scanResult.track_count -#: src/stores/useLibraryStore.ts:98 +#: src/stores/useLibraryStore.ts:101 msgid "{0} track(s) were refreshed." msgstr "{0} track(s) were refreshed." #. placeholder {0}: scanResult.track_count -#: src/stores/useLibraryStore.ts:99 +#: src/stores/useLibraryStore.ts:102 msgid "{0} track(s) were added to the library." msgstr "{0} track(s) were added to the library." #. placeholder {0}: scanResult.playlist_count -#: src/stores/useLibraryStore.ts:109 +#: src/stores/useLibraryStore.ts:112 msgid "{0} playlist(s) were added to the library." msgstr "{0} playlist(s) were added to the library." -#: src/stores/useLibraryStore.ts:170 +#: src/stores/useLibraryStore.ts:173 msgid "Library was reset" msgstr "Library was reset" diff --git a/src/translations/fr.po b/src/translations/fr.po index 6c5c38b03..5af149964 100644 --- a/src/translations/fr.po +++ b/src/translations/fr.po @@ -66,7 +66,7 @@ msgstr "Artistes" #: src/components/Footer.tsx:39 #: src/routes/playlists.tsx:169 -#: src/routes/settings.ui.tsx:126 +#: src/routes/settings.ui.tsx:127 msgid "Playlists" msgstr "Playlists" @@ -212,8 +212,8 @@ msgstr "Annuler" msgid "Remove" msgstr "Retirer" -#. placeholder {0}: props.count #. placeholder {0}: tracks.length +#. placeholder {0}: props.count #: src/components/TrackListGrouped.tsx:102 #: src/components/TrackListStatus.tsx:11 msgid "{0, plural, one {# track} other {# tracks}}" @@ -446,10 +446,10 @@ msgstr "Toutes vos pistes et playlists seront supprimées de Museeks." #: src/routes/settings.library.tsx:140 msgid "Reset library?" -msgstr "Réinitialiser la bibliothèque ?" +msgstr "Réinitialiser la bibliothèque ?<<<<<<< HEAD" #: src/routes/settings.library.tsx:143 -#: src/routes/settings.ui.tsx:65 +#: src/routes/settings.ui.tsx:66 msgid "Reset" msgstr "Réinitialiser" @@ -469,75 +469,75 @@ msgstr "Interface" msgid "About" msgstr "À propos" -#: src/routes/settings.ui.tsx:36 +#: src/routes/settings.ui.tsx:37 msgid "Theme" msgstr "Thème" -#: src/routes/settings.ui.tsx:37 +#: src/routes/settings.ui.tsx:38 msgid "Change the appearance of the interface" msgstr "Changer l'apparence de l'interface" -#: src/routes/settings.ui.tsx:43 +#: src/routes/settings.ui.tsx:44 msgid "System (default)" msgstr "Système (par défaut)" -#: src/routes/settings.ui.tsx:55 +#: src/routes/settings.ui.tsx:56 msgid "Accent color" msgstr "Couleur d'accentuation" -#: src/routes/settings.ui.tsx:76 +#: src/routes/settings.ui.tsx:77 msgid "Language" msgstr "Langue" -#: src/routes/settings.ui.tsx:94 +#: src/routes/settings.ui.tsx:95 msgid "Tracks density" msgstr "Densité visuelle des pistes" -#: src/routes/settings.ui.tsx:95 +#: src/routes/settings.ui.tsx:96 msgid "Change the tracks spacing" msgstr "Adapter l'espace entre les pistes" -#: src/routes/settings.ui.tsx:104 +#: src/routes/settings.ui.tsx:105 msgid "Normal (default)" msgstr "Normal (par défaut)" -#: src/routes/settings.ui.tsx:107 +#: src/routes/settings.ui.tsx:108 msgid "Compact" msgstr "Compact" -#: src/routes/settings.ui.tsx:113 +#: src/routes/settings.ui.tsx:114 msgid "Default view" msgstr "Vue par défaut" -#: src/routes/settings.ui.tsx:115 +#: src/routes/settings.ui.tsx:116 msgid "Change the default view when starting the application" msgstr "Changer la vue par défaut au démarrage de l'application" -#: src/routes/settings.ui.tsx:123 +#: src/routes/settings.ui.tsx:124 msgid "Library (default)" -msgstr "Bibliothèque (par défaut)" +msgstr "Bibliothèque (par défaut)<<<<<<< HEAD" -#: src/routes/settings.ui.tsx:132 +#: src/routes/settings.ui.tsx:133 msgid "Display Notifications" msgstr "Afficher les notifications" -#: src/routes/settings.ui.tsx:133 +#: src/routes/settings.ui.tsx:134 msgid "Send notifications when the playing track changes" msgstr "Envoyer des notifications lorsque la piste en cours change" -#: src/routes/settings.ui.tsx:142 +#: src/routes/settings.ui.tsx:143 msgid "Sleep mode blocker" msgstr "Bloqueur de mode veille" -#: src/routes/settings.ui.tsx:143 +#: src/routes/settings.ui.tsx:144 msgid "Prevent the computer from going into sleep mode when playing" msgstr "Empêcher l'ordinateur de passer en mode veille lors de la lecture" -#: src/routes/settings.ui.tsx:158 +#: src/routes/settings.ui.tsx:159 msgid "Light" msgstr "Clair" -#: src/routes/settings.ui.tsx:160 +#: src/routes/settings.ui.tsx:161 msgid "Dark" msgstr "Sombre" @@ -596,29 +596,29 @@ msgid "The playlist \"{name}\" was created" msgstr "La playlist \"{name}\" a été créée" #. placeholder {0}: newRelease.tag_name -#: src/stores/SettingsAPI.ts:171 +#: src/stores/SettingsAPI.ts:170 msgid "Museeks {0} is available, check https://museeks.io!" msgstr "Museeks {0} est disponible, consultez https://museeks.io !" -#: src/stores/SettingsAPI.ts:173 +#: src/stores/SettingsAPI.ts:172 msgid "Museeks {currentVersion} is the latest version available." msgstr "Museeks {currentVersion} est la dernière version disponible." #. placeholder {0}: scanResult.track_count -#: src/stores/useLibraryStore.ts:98 +#: src/stores/useLibraryStore.ts:101 msgid "{0} track(s) were refreshed." msgstr "{0} piste(s) ont été actualisée(s)." #. placeholder {0}: scanResult.track_count -#: src/stores/useLibraryStore.ts:99 +#: src/stores/useLibraryStore.ts:102 msgid "{0} track(s) were added to the library." msgstr "{0} piste(s) ont été ajoutée(s) à la bibliothèque." #. placeholder {0}: scanResult.playlist_count -#: src/stores/useLibraryStore.ts:109 +#: src/stores/useLibraryStore.ts:112 msgid "{0} playlist(s) were added to the library." msgstr "{0} playlist(s) ont été ajoutée(s) à la bibliothèque." -#: src/stores/useLibraryStore.ts:170 +#: src/stores/useLibraryStore.ts:173 msgid "Library was reset" msgstr "La bibliothèque a été réinitialisée" diff --git a/src/translations/ru.po b/src/translations/ru.po index 517ba28cc..2b937340c 100644 --- a/src/translations/ru.po +++ b/src/translations/ru.po @@ -66,7 +66,7 @@ msgstr "Исполнители" #: src/components/Footer.tsx:39 #: src/routes/playlists.tsx:169 -#: src/routes/settings.ui.tsx:126 +#: src/routes/settings.ui.tsx:127 msgid "Playlists" msgstr "Плейлисты" @@ -212,8 +212,8 @@ msgstr "Отмена" msgid "Remove" msgstr "Удалить" -#. placeholder {0}: props.count #. placeholder {0}: tracks.length +#. placeholder {0}: props.count #: src/components/TrackListGrouped.tsx:102 #: src/components/TrackListStatus.tsx:11 msgid "{0, plural, one {# track} other {# tracks}}" @@ -446,10 +446,10 @@ msgstr "Все ваши треки и плейлисты будут удален #: src/routes/settings.library.tsx:140 msgid "Reset library?" -msgstr "Сбросить библиотеку?" +msgstr "Сбросить библиотеку?<<<<<<< HEAD" #: src/routes/settings.library.tsx:143 -#: src/routes/settings.ui.tsx:65 +#: src/routes/settings.ui.tsx:66 msgid "Reset" msgstr "Сбросить" @@ -469,75 +469,75 @@ msgstr "Интерфейс" msgid "About" msgstr "О программе" -#: src/routes/settings.ui.tsx:36 +#: src/routes/settings.ui.tsx:37 msgid "Theme" msgstr "Тема" -#: src/routes/settings.ui.tsx:37 +#: src/routes/settings.ui.tsx:38 msgid "Change the appearance of the interface" msgstr "Изменить внешний вид интерфейса" -#: src/routes/settings.ui.tsx:43 +#: src/routes/settings.ui.tsx:44 msgid "System (default)" msgstr "Системная (по умолчанию)" -#: src/routes/settings.ui.tsx:55 +#: src/routes/settings.ui.tsx:56 msgid "Accent color" msgstr "Цвет акцента" -#: src/routes/settings.ui.tsx:76 +#: src/routes/settings.ui.tsx:77 msgid "Language" msgstr "Язык" -#: src/routes/settings.ui.tsx:94 +#: src/routes/settings.ui.tsx:95 msgid "Tracks density" msgstr "Плотность треков" -#: src/routes/settings.ui.tsx:95 +#: src/routes/settings.ui.tsx:96 msgid "Change the tracks spacing" msgstr "Изменить расстояние между треками" -#: src/routes/settings.ui.tsx:104 +#: src/routes/settings.ui.tsx:105 msgid "Normal (default)" msgstr "Номральная (по умолчанию)" -#: src/routes/settings.ui.tsx:107 +#: src/routes/settings.ui.tsx:108 msgid "Compact" msgstr "Компатная" -#: src/routes/settings.ui.tsx:113 +#: src/routes/settings.ui.tsx:114 msgid "Default view" msgstr "Представление по умолчанию" -#: src/routes/settings.ui.tsx:115 +#: src/routes/settings.ui.tsx:116 msgid "Change the default view when starting the application" msgstr "Изменить представление по умолчанию при запуске приложения" -#: src/routes/settings.ui.tsx:123 +#: src/routes/settings.ui.tsx:124 msgid "Library (default)" -msgstr "Библиотека (по умолчанию)" +msgstr "Библиотека (по умолчанию)<<<<<<< HEAD" -#: src/routes/settings.ui.tsx:132 +#: src/routes/settings.ui.tsx:133 msgid "Display Notifications" msgstr "Отображать уведомления" -#: src/routes/settings.ui.tsx:133 +#: src/routes/settings.ui.tsx:134 msgid "Send notifications when the playing track changes" msgstr "Посылать уведомления при смене проигрываемого трека" -#: src/routes/settings.ui.tsx:142 +#: src/routes/settings.ui.tsx:143 msgid "Sleep mode blocker" msgstr "Блокировать режим сна" -#: src/routes/settings.ui.tsx:143 +#: src/routes/settings.ui.tsx:144 msgid "Prevent the computer from going into sleep mode when playing" msgstr "Предпятствовать переходу компьютера в режим сна во время проигрывания" -#: src/routes/settings.ui.tsx:158 +#: src/routes/settings.ui.tsx:159 msgid "Light" msgstr "Светлая" -#: src/routes/settings.ui.tsx:160 +#: src/routes/settings.ui.tsx:161 msgid "Dark" msgstr "Тёмная" @@ -596,29 +596,29 @@ msgid "The playlist \"{name}\" was created" msgstr "Создан плейлист \"{name}\"" #. placeholder {0}: newRelease.tag_name -#: src/stores/SettingsAPI.ts:171 +#: src/stores/SettingsAPI.ts:170 msgid "Museeks {0} is available, check https://museeks.io!" msgstr "Доступна версия Museeks {0}, проверьте https://museeks.io!" -#: src/stores/SettingsAPI.ts:173 +#: src/stores/SettingsAPI.ts:172 msgid "Museeks {currentVersion} is the latest version available." msgstr "Museeks {currentVersion} является самой последней доступной версией." #. placeholder {0}: scanResult.track_count -#: src/stores/useLibraryStore.ts:98 +#: src/stores/useLibraryStore.ts:101 msgid "{0} track(s) were refreshed." msgstr "{0} трек(ов) обновлено." #. placeholder {0}: scanResult.track_count -#: src/stores/useLibraryStore.ts:99 +#: src/stores/useLibraryStore.ts:102 msgid "{0} track(s) were added to the library." msgstr "{0} трек(ов) добавлено в библиотеку." #. placeholder {0}: scanResult.playlist_count -#: src/stores/useLibraryStore.ts:109 +#: src/stores/useLibraryStore.ts:112 msgid "{0} playlist(s) were added to the library." msgstr "{0} плейлист(ов) добавлено в библиотеку." -#: src/stores/useLibraryStore.ts:170 +#: src/stores/useLibraryStore.ts:173 msgid "Library was reset" msgstr "Библиотека сброшена" diff --git a/tests/example.spec.ts b/tests/example.spec.ts new file mode 100644 index 000000000..0f701a82f --- /dev/null +++ b/tests/example.spec.ts @@ -0,0 +1,8 @@ +import { expect, test } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('/#library'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Museeks/); +});