mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
refactor(ipc): complete tauri-specta typed-IPC cutover, contract guards, and layering cleanup (#1230)
This commit is contained in:
@@ -15,7 +15,7 @@ import type { CoverPrefetchPriority } from '@/cover/types';
|
||||
import { COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '@/cover/layoutSizes';
|
||||
import { resolveCoverDisplayTier } from '@/cover/tiers';
|
||||
import { acquireUrl } from '@/cover';
|
||||
import { OpenArtistRefInline } from '@/features/artist';
|
||||
import { OpenArtistRefInline } from '@/ui/OpenArtistRefInline';
|
||||
import { fetchAlbumTracks, playAlbum, playAlbumShuffled } from '@/features/playback/utils/playback/playAlbum';
|
||||
import { useLongPressAction } from '@/lib/hooks/useLongPressAction';
|
||||
import { LongPressWaveOverlay } from '@/ui/LongPressWaveOverlay';
|
||||
|
||||
@@ -19,7 +19,7 @@ vi.mock('@/features/album/hooks/useAlbumDetailBack', () => ({ useAlbumDetailBack
|
||||
vi.mock('@/lib/hooks/useIsMobile', () => ({ useIsMobile: () => false }));
|
||||
vi.mock('@/store/themeStore', () => ({ useThemeStore: () => false }));
|
||||
vi.mock('@/ui/StarRating', () => ({ default: () => null }));
|
||||
vi.mock('@/features/artist', () => ({ OpenArtistRefInline: () => null }));
|
||||
vi.mock('@/ui/OpenArtistRefInline', () => ({ OpenArtistRefInline: () => null }));
|
||||
vi.mock('@/cover/CoverArtImage', () => ({ CoverArtImage: () => null }));
|
||||
|
||||
function baseProps() {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { isAlbumRecentlyAdded } from '@/features/album/utils/albumRecency';
|
||||
import { formatLongDuration } from '@/lib/format/formatDuration';
|
||||
import { formatMb } from '@/lib/format/formatBytes';
|
||||
import { sanitizeHtml } from '@/lib/util/sanitizeHtml';
|
||||
import { OpenArtistRefInline } from '@/features/artist';
|
||||
import { OpenArtistRefInline } from '@/ui/OpenArtistRefInline';
|
||||
import { tooltipAttrs } from '@/ui/tooltipAttrs';
|
||||
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
|
||||
import { deriveAlbumGenreTags } from '@/lib/library/genreTags';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { songToTrack } from '@/lib/media/songToTrack';
|
||||
import { shuffleArray } from '@/lib/util/shuffleArray';
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { downloadZip } from '@/lib/api/downloadZip';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useOrbitSongRowBehavior } from '@/features/orbit';
|
||||
@@ -245,7 +245,7 @@ const handleShuffleAll = () => {
|
||||
const { start, complete, fail } = useZipDownloadStore.getState();
|
||||
start(downloadId, filename);
|
||||
try {
|
||||
await invoke('download_zip', { id: downloadId, url, destPath });
|
||||
await downloadZip({ id: downloadId, url, destPath });
|
||||
complete(downloadId);
|
||||
} catch (e) {
|
||||
fail(downloadId);
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useOfflineStore } from '@/features/offline';
|
||||
import { useDownloadModalStore } from '@/features/offline';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { downloadZip } from '@/lib/api/downloadZip';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
import { useZipDownloadStore } from '@/features/offline';
|
||||
@@ -272,7 +272,7 @@ export default function Albums() {
|
||||
const url = buildDownloadUrl(album.id);
|
||||
start(downloadId, filename);
|
||||
try {
|
||||
await invoke('download_zip', { id: downloadId, url, destPath });
|
||||
await downloadZip({ id: downloadId, url, destPath });
|
||||
complete(downloadId);
|
||||
} catch (e) {
|
||||
fail(downloadId);
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useRangeSelection } from '@/lib/hooks/useRangeSelection';
|
||||
import { useMainstageInpageHeaderTight } from '@/lib/hooks/useMainstageInpageHeaderTight';
|
||||
import { usePerfProbeFlags } from '@/lib/perf/perfFlags';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { downloadZip } from '@/lib/api/downloadZip';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { Download, HardDriveDownload, ListPlus } from 'lucide-react';
|
||||
import SelectionToggleButton from '@/ui/SelectionToggleButton';
|
||||
@@ -280,7 +280,7 @@ export default function LosslessAlbums() {
|
||||
const url = buildDownloadUrl(album.id);
|
||||
start(downloadId, filename);
|
||||
try {
|
||||
await invoke('download_zip', { id: downloadId, url, destPath });
|
||||
await downloadZip({ id: downloadId, url, destPath });
|
||||
complete(downloadId);
|
||||
} catch (e) {
|
||||
fail(downloadId);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useOfflineStore } from '@/features/offline';
|
||||
import { useDownloadModalStore } from '@/features/offline';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { downloadZip } from '@/lib/api/downloadZip';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
import { useZipDownloadStore } from '@/features/offline';
|
||||
@@ -148,7 +148,7 @@ export default function NewReleases() {
|
||||
const url = buildDownloadUrl(album.id);
|
||||
start(downloadId, filename);
|
||||
try {
|
||||
await invoke('download_zip', { id: downloadId, url, destPath });
|
||||
await downloadZip({ id: downloadId, url, destPath });
|
||||
complete(downloadId);
|
||||
} catch (e) {
|
||||
fail(downloadId);
|
||||
|
||||
@@ -18,7 +18,7 @@ import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '@/feat
|
||||
import { runLocalRandomAlbums, runLocalAlbumsByGenres } from '@/lib/library/browseTextSearch';
|
||||
import { useOfflineStore } from '@/features/offline';
|
||||
import { useDownloadModalStore } from '@/features/offline';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { downloadZip } from '@/lib/api/downloadZip';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
import { useZipDownloadStore } from '@/features/offline';
|
||||
@@ -188,7 +188,7 @@ export default function RandomAlbums() {
|
||||
const url = buildDownloadUrl(album.id);
|
||||
start(downloadId, filename);
|
||||
try {
|
||||
await invoke('download_zip', { id: downloadId, url, destPath });
|
||||
await downloadZip({ id: downloadId, url, destPath });
|
||||
complete(downloadId);
|
||||
} catch (e) {
|
||||
fail(downloadId);
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import type { SubsonicOpenArtistRef } from '@/lib/api/subsonicTypes';
|
||||
|
||||
interface Props {
|
||||
refs: SubsonicOpenArtistRef[];
|
||||
/** Used when `refs` is empty (callers should normally avoid that). */
|
||||
fallbackName: string;
|
||||
/** Invoked with Subsonic artist id when a ref has an id. */
|
||||
onGoArtist: (artistId: string) => void;
|
||||
/** Wrapper element: `span` (default) or `fragment` children only. */
|
||||
as?: 'span' | 'none';
|
||||
/** `button` for album header; `span` matches dense player / track rows. */
|
||||
linkTag?: 'button' | 'span';
|
||||
outerClassName?: string;
|
||||
linkClassName?: string;
|
||||
separatorClassName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders OpenSubsonic `artists` / `albumArtists` refs as ·-separated names with
|
||||
* per-artist navigation when `id` is present (same interaction model as album
|
||||
* track rows).
|
||||
*/
|
||||
export function OpenArtistRefInline({
|
||||
refs,
|
||||
fallbackName,
|
||||
onGoArtist,
|
||||
as = 'span',
|
||||
linkTag = 'button',
|
||||
outerClassName,
|
||||
linkClassName,
|
||||
separatorClassName = 'open-artist-ref-sep',
|
||||
}: Props) {
|
||||
const list = refs.length > 0 ? refs : [{ name: fallbackName }];
|
||||
const inner = (
|
||||
<>
|
||||
{list.map((a, i) => (
|
||||
<Fragment key={a.id ?? `n:${a.name ?? ''}:${i}`}>
|
||||
{i > 0 && <span className={separatorClassName} aria-hidden="true"> · </span>}
|
||||
{a.id ? (
|
||||
linkTag === 'span' ? (
|
||||
<span
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
className={linkClassName}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onGoArtist(a.id!);
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onGoArtist(a.id!);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{a.name ?? fallbackName}
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={linkClassName}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onGoArtist(a.id!);
|
||||
}}
|
||||
>
|
||||
{a.name ?? fallbackName}
|
||||
</button>
|
||||
)
|
||||
) : (
|
||||
<span>{a.name ?? fallbackName}</span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
if (as === 'none') return inner;
|
||||
return <span className={outerClassName}>{inner}</span>;
|
||||
}
|
||||
@@ -2,9 +2,10 @@
|
||||
* Artist feature — the Artists overview + ArtistDetail (both lazy via their deep
|
||||
* `pages/*` paths, not re-exported), artist cards/rows/avatars, the detail hero +
|
||||
* similar-artists + top-tracks UI, the artist Subsonic API, browse catalog/
|
||||
* filter/scroll hooks, per-artist offline status, and the open-artist-ref nav
|
||||
* cluster. `sortArtistAlbums` moved in from `lib/library/` (artist-only sort,
|
||||
* not the shared index query engine).
|
||||
* filter/scroll hooks, and per-artist offline status. `sortArtistAlbums` moved
|
||||
* in from `lib/library/` (artist-only sort, not the shared index query engine).
|
||||
* `OpenArtistRefInline` moved out to `ui/` (a shared presentational primitive,
|
||||
* consumed by ~10 features) to break the album ⇄ artist import cycle.
|
||||
*
|
||||
* Stays OUT (other owners): `deriveAlbumHeaderArtistRefs` (album header),
|
||||
* `playArtistShuffled` + `trackArtistRefs` (playback/track helpers), the
|
||||
@@ -30,7 +31,6 @@ export * from './utils/artistsHelpers';
|
||||
export * from './utils/runArtistDetailActions';
|
||||
export * from './utils/runArtistDetailPlay';
|
||||
export * from './utils/sortArtistAlbums';
|
||||
export { OpenArtistRefInline } from './components/OpenArtistRefInline';
|
||||
export { default as ArtistCardLocal } from './components/ArtistCardLocal';
|
||||
export { default as ArtistDetailHero } from './components/ArtistDetailHero';
|
||||
export { default as ArtistDetailSimilarArtists } from './components/ArtistDetailSimilarArtists';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { downloadZip } from '@/lib/api/downloadZip';
|
||||
import { getSimilarSongs2, fetchSimilarTracksRouted, getTopSongs } from '@/lib/api/subsonicArtists';
|
||||
import { filterSongsForLuckyMixRatings, getMixMinRatingsConfigFromAuth } from '@/features/playback/utils/mixRatingFilter';
|
||||
import { buildDownloadUrl } from '@/lib/api/subsonicStreamUrl';
|
||||
@@ -147,7 +147,7 @@ export async function downloadAlbum(albumName: string, albumId: string) {
|
||||
const { start, complete, fail } = useZipDownloadStore.getState();
|
||||
start(id, filename);
|
||||
try {
|
||||
await invoke('download_zip', { id, url, destPath });
|
||||
await downloadZip({ id, url, destPath });
|
||||
complete(id);
|
||||
} catch (e) {
|
||||
fail(id);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { cancelDeviceSync } from '@/lib/api/syncfs';
|
||||
import {
|
||||
AlertCircle, CheckCircle2, Clock, HardDriveUpload, Loader2,
|
||||
Trash2, Undo2,
|
||||
@@ -200,7 +200,7 @@ export default function DeviceSyncDevicePanel({
|
||||
style={{ fontSize: 12, padding: '2px 10px' }}
|
||||
onClick={() => {
|
||||
const jobId = useDeviceSyncJobStore.getState().jobId;
|
||||
if (jobId) invoke('cancel_device_sync', { jobId });
|
||||
if (jobId) cancelDeviceSync({ jobId });
|
||||
useDeviceSyncJobStore.getState().cancel();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listDeviceDirFiles } from '@/lib/api/syncfs';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { useDeviceSyncStore, type DeviceSyncSource } from '@/features/deviceSync/store/deviceSyncStore';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
@@ -24,7 +25,7 @@ export function useDeviceSyncDeviceScan(
|
||||
}
|
||||
setScanning(true);
|
||||
try {
|
||||
const files = await invoke<string[]>('list_device_dir_files', { dir: targetDir });
|
||||
const files = await listDeviceDirFiles({ dir: targetDir });
|
||||
setDeviceFilePaths(files);
|
||||
} catch {
|
||||
setDeviceFilePaths([]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { getRemovableDrives } from '@/lib/api/syncfs';
|
||||
import type { RemovableDrive } from '@/features/deviceSync/utils/deviceSyncHelpers';
|
||||
|
||||
export interface DeviceSyncDrivesResult {
|
||||
@@ -17,7 +17,7 @@ export function useDeviceSyncDrives(targetDir: string | null): DeviceSyncDrivesR
|
||||
const refreshDrives = useCallback(async () => {
|
||||
setDrivesLoading(true);
|
||||
try {
|
||||
const result = await invoke<RemovableDrive[]>('get_removable_drives');
|
||||
const result = await getRemovableDrives();
|
||||
setDrives(result);
|
||||
} catch {
|
||||
setDrives([]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { computeSyncPaths } from '@/lib/api/syncfs';
|
||||
import { fetchTracksForSource } from '@/features/playback/utils/playback/fetchTracksForSource';
|
||||
import { trackToSyncInfo, type SyncStatus } from '@/features/deviceSync/utils/deviceSyncHelpers';
|
||||
import type { DeviceSyncSource } from '@/features/deviceSync/store/deviceSyncStore';
|
||||
@@ -34,7 +34,7 @@ export function useDeviceSyncSourceStatuses(
|
||||
if (cancelled) return;
|
||||
try {
|
||||
const tracks = await fetchTracksForSource(source);
|
||||
const paths = await invoke<string[]>('compute_sync_paths', {
|
||||
const paths = await computeSyncPaths({
|
||||
tracks: tracks.map((tr, idx) => trackToSyncInfo(
|
||||
tr, '',
|
||||
source.type === 'playlist' ? { name: source.name, index: idx + 1 } : undefined,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import type { TrackSyncInfo } from '@/generated/bindings';
|
||||
|
||||
export type SourceTab = 'playlists' | 'albums' | 'artists';
|
||||
|
||||
@@ -6,14 +7,7 @@ export function uuid(): string { return crypto.randomUUID(); }
|
||||
|
||||
export type SyncStatus = 'synced' | 'pending' | 'deletion';
|
||||
|
||||
export interface RemovableDrive {
|
||||
name: string;
|
||||
mount_point: string;
|
||||
available_space: number;
|
||||
total_space: number;
|
||||
file_system: string;
|
||||
is_removable: boolean;
|
||||
}
|
||||
export type { RemovableDrive } from '@/generated/bindings';
|
||||
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
@@ -32,7 +26,7 @@ export function trackToSyncInfo(
|
||||
track: SyncTrackMaybePlaylist,
|
||||
url: string,
|
||||
playlistCtx?: { name: string; index: number },
|
||||
) {
|
||||
): TrackSyncInfo {
|
||||
// Fall back to track artist when the file has no albumArtist tag — not every
|
||||
// library is tagged with it. Treat empty strings as missing (some Subsonic
|
||||
// servers return "" rather than omitting the field).
|
||||
@@ -44,7 +38,7 @@ export function trackToSyncInfo(
|
||||
albumArtist,
|
||||
album: track.album ?? '',
|
||||
title: track.title ?? '',
|
||||
trackNumber: track.track,
|
||||
trackNumber: track.track ?? null,
|
||||
duration: track.duration,
|
||||
playlistName: playlistCtx?.name ?? track._playlistName,
|
||||
playlistIndex: playlistCtx?.index ?? track._playlistIndex,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TFunction } from 'i18next';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { computeSyncPaths, deleteDeviceFiles, syncBatchToDevice } from '@/lib/api/syncfs';
|
||||
import { buildDownloadUrl } from '@/lib/api/subsonicStreamUrl';
|
||||
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import { useDeviceSyncStore, type DeviceSyncSource } from '@/features/deviceSync/store/deviceSyncStore';
|
||||
@@ -68,6 +69,7 @@ export interface RunDeviceSyncExecuteDeps {
|
||||
|
||||
export async function runDeviceSyncExecute(deps: RunDeviceSyncExecuteDeps): Promise<void> {
|
||||
const { targetDir, sources, pendingDeletion, syncDelta, t, setPreSyncOpen, removeSources, scanDevice } = deps;
|
||||
if (!targetDir) return;
|
||||
|
||||
setPreSyncOpen(false);
|
||||
|
||||
@@ -80,7 +82,7 @@ export async function runDeviceSyncExecute(deps: RunDeviceSyncExecuteDeps): Prom
|
||||
// folder (Playlists/{Name}/…) rather than from the album tree.
|
||||
for (const source of deletionSources) {
|
||||
const tracks = await fetchTracksForSource(source);
|
||||
const paths = await invoke<string[]>('compute_sync_paths', {
|
||||
const paths = await computeSyncPaths({
|
||||
tracks: tracks.map((tr, idx) => trackToSyncInfo(
|
||||
tr, '',
|
||||
source.type === 'playlist' ? { name: source.name, index: idx + 1 } : undefined,
|
||||
@@ -90,7 +92,7 @@ export async function runDeviceSyncExecute(deps: RunDeviceSyncExecuteDeps): Prom
|
||||
allPaths.push(...paths);
|
||||
}
|
||||
|
||||
await invoke<number>('delete_device_files', { paths: allPaths });
|
||||
await deleteDeviceFiles({ paths: allPaths });
|
||||
removeSources(deletionSources.map(s => s.id));
|
||||
// Update manifest so it stays in sync after deletions
|
||||
const remainingSources = useDeviceSyncStore.getState().sources;
|
||||
@@ -130,16 +132,19 @@ export async function runDeviceSyncExecute(deps: RunDeviceSyncExecuteDeps): Prom
|
||||
|
||||
showToast(t('deviceSync.syncInBackground'), 3000, 'info');
|
||||
|
||||
invoke('sync_batch_to_device', {
|
||||
syncBatchToDevice({
|
||||
tracks: allTracks.map(track => trackToSyncInfo(track, buildDownloadUrl(track.id))),
|
||||
destDir: targetDir,
|
||||
jobId,
|
||||
expectedBytes: syncDelta.addBytes,
|
||||
}).catch((err: string) => {
|
||||
}).catch((err: unknown) => {
|
||||
// The typed facade rejects with an Error whose message is the raw Rust error
|
||||
// string (previously invoke rejected with the bare string).
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
useDeviceSyncJobStore.getState().complete(0, 0, allTracks.length);
|
||||
if (err.includes('NOT_ENOUGH_SPACE')) {
|
||||
if (msg.includes('NOT_ENOUGH_SPACE')) {
|
||||
showToast(t('deviceSync.notEnoughSpace'), 5000, 'error');
|
||||
} else if (err === 'NOT_MOUNTED_VOLUME') {
|
||||
} else if (msg === 'NOT_MOUNTED_VOLUME') {
|
||||
showToast(t('deviceSync.notMountedVolume'), 5000, 'error');
|
||||
} else {
|
||||
showToast(t('deviceSync.fetchError'), 3000, 'error');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type React from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { computeSyncPaths } from '@/lib/api/syncfs';
|
||||
import type { DeviceSyncSource } from '@/features/deviceSync/store/deviceSyncStore';
|
||||
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import { applyLegacyTemplate } from '@/features/deviceSync/utils/deviceSyncLegacyTemplate';
|
||||
@@ -64,7 +65,7 @@ export async function runDeviceSyncMigrationPreview(deps: RunMigrationPreviewDep
|
||||
}
|
||||
|
||||
// New paths via Rust (fixed album-tree schema).
|
||||
const newAbsPaths = await invoke<string[]>('compute_sync_paths', {
|
||||
const newAbsPaths = await computeSyncPaths({
|
||||
tracks: albumSourceTracks.map(tr => trackToSyncInfo(tr, '')),
|
||||
destDir: targetDir,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { commands } from '@/generated/bindings';
|
||||
import { useEqStore } from '@/store/eqStore';
|
||||
import { parseFixedBandEqString, type AutoEqVariant, type AutoEqResult } from '@/features/playback/utils/audio/autoEqParse';
|
||||
|
||||
@@ -26,7 +26,9 @@ export function useAutoEq() {
|
||||
setEntriesLoading(true);
|
||||
setAutoEqError(null);
|
||||
try {
|
||||
const json = await invoke<string>('autoeq_entries');
|
||||
const entriesRes = await commands.autoeqEntries();
|
||||
if (entriesRes.status === 'error') throw new Error(entriesRes.error);
|
||||
const json = entriesRes.data;
|
||||
entriesCacheRef.current = JSON.parse(json);
|
||||
} catch (e: unknown) {
|
||||
setAutoEqError(e instanceof Error ? e.message : t('settings.eqAutoEqError'));
|
||||
@@ -56,12 +58,14 @@ export function useAutoEq() {
|
||||
setAutoEqLoading(true);
|
||||
setAutoEqError(null);
|
||||
try {
|
||||
const text = await invoke<string>('autoeq_fetch_profile', {
|
||||
name: result.name,
|
||||
source: result.source,
|
||||
rig: result.rig ?? null,
|
||||
form: result.form,
|
||||
});
|
||||
const fetchRes = await commands.autoeqFetchProfile(
|
||||
result.name,
|
||||
result.source,
|
||||
result.rig ?? null,
|
||||
result.form,
|
||||
);
|
||||
if (fetchRes.status === 'error') throw new Error(fetchRes.error);
|
||||
const text = fetchRes.data;
|
||||
if (!text) throw new Error(t('settings.eqAutoEqFetchError'));
|
||||
const { gains: newGains, preamp } = parseFixedBandEqString(text);
|
||||
applyAutoEq(result.name, newGains, preamp);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { formatLastSeen } from '@/lib/format/userMgmtHelpers';
|
||||
import i18n from '@/lib/i18n';
|
||||
import { formatTrackTime } from '@/lib/format/formatDuration';
|
||||
import StarRating from '@/ui/StarRating';
|
||||
import { OpenArtistRefInline } from '@/features/artist';
|
||||
import { OpenArtistRefInline } from '@/ui/OpenArtistRefInline';
|
||||
import { resolveTrackArtistRefs } from '@/features/playback/utils/playback/trackArtistRefs';
|
||||
|
||||
export interface FavoriteSongRowCallbacks {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useDragDrop } from '@/lib/dnd/DragDropContext';
|
||||
import { useOrbitSongRowBehavior } from '@/features/orbit';
|
||||
import { useNavigateToAlbum } from '@/features/album';
|
||||
import { useNavigateToArtist } from '@/features/artist';
|
||||
import { OpenArtistRefInline } from '@/features/artist';
|
||||
import { OpenArtistRefInline } from '@/ui/OpenArtistRefInline';
|
||||
import { resolveTrackArtistRefs } from '@/features/playback/utils/playback/trackArtistRefs';
|
||||
|
||||
interface SongCardProps {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { commands } from '@/generated/bindings';
|
||||
|
||||
/** Fetches a synced LRC string from Netease Cloud Music via Rust proxy. Returns null if not found. */
|
||||
export async function fetchNeteaselyrics(artist: string, title: string): Promise<string | null> {
|
||||
try {
|
||||
return await invoke<string | null>('fetch_netease_lyrics', { artist, title });
|
||||
const res = await commands.fetchNeteaseLyrics(artist, title);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
return res.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { SubsonicStructuredLyrics } from '@/lib/api/subsonicTypes';
|
||||
import type { Track } from '@/lib/media/trackTypes';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { commands } from '@/generated/bindings';
|
||||
import { fetchLyrics, parseLrc, LrcLine } from '@/features/lyrics/api/lrclib';
|
||||
import { fetchNeteaselyrics } from '@/features/lyrics/api/netease';
|
||||
import { fetchLyricsPlus, hasWordSync } from '@/features/lyrics/api/lyricsplus';
|
||||
@@ -158,7 +158,7 @@ export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
||||
if (!filePath) return false;
|
||||
|
||||
try {
|
||||
const lrcString = await invoke<string | null>('get_embedded_lyrics', { path: filePath });
|
||||
const lrcString = await commands.getEmbeddedLyrics(filePath);
|
||||
if (!lrcString) return false;
|
||||
|
||||
const lines = parseLrc(lrcString);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import CachedImage from '@/ui/CachedImage';
|
||||
import { OpenArtistRefInline } from '@/features/artist';
|
||||
import { OpenArtistRefInline } from '@/ui/OpenArtistRefInline';
|
||||
import type { MiniTrackInfo } from '@/features/miniPlayer/utils/miniPlayerBridge';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { usePlaybackCoverArt } from '@/cover/usePlaybackCoverArt';
|
||||
import { usePlaybackTrackCoverRef } from '@/cover/useLibraryCoverRef';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { closeMiniPlayer, resizeMiniPlayer, setMiniPlayerAlwaysOnTop, showMainWindow } from '@/lib/api/miniPlayer';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { registerQueueDragHitTest } from '@/lib/dnd/DragDropContext';
|
||||
@@ -95,14 +95,14 @@ export default function MiniPlayer() {
|
||||
const toggleOnTop = async () => {
|
||||
const next = !alwaysOnTop;
|
||||
setAlwaysOnTop(next);
|
||||
try { await invoke('set_mini_player_always_on_top', { onTop: next }); } catch { /* ignore: best-effort */ }
|
||||
try { await setMiniPlayerAlwaysOnTop({ onTop: next }); } catch { /* ignore: best-effort */ }
|
||||
};
|
||||
|
||||
const closeMini = async () => {
|
||||
try { await invoke('close_mini_player'); } catch { /* ignore: best-effort */ }
|
||||
try { await closeMiniPlayer(); } catch { /* ignore: best-effort */ }
|
||||
};
|
||||
|
||||
const showMain = () => invoke('show_main_window').catch(() => {});
|
||||
const showMain = () => showMainWindow().catch(() => {});
|
||||
|
||||
const toggleQueue = async () => {
|
||||
const next = !queueOpen;
|
||||
@@ -121,7 +121,7 @@ export default function MiniPlayer() {
|
||||
const targetW = next ? EXPANDED_SIZE.w : COLLAPSED_SIZE.w;
|
||||
const min = next ? EXPANDED_MIN : COLLAPSED_MIN;
|
||||
try {
|
||||
await invoke('resize_mini_player', {
|
||||
await resizeMiniPlayer({
|
||||
width: targetW,
|
||||
height: targetH,
|
||||
minWidth: min.w,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { setLinuxWebkitSmoothScrolling } from '@/lib/api/platformShell';
|
||||
import { resizeMiniPlayer, setMiniPlayerAlwaysOnTop } from '@/lib/api/miniPlayer';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { IS_LINUX } from '@/lib/util/platform';
|
||||
import {
|
||||
@@ -18,7 +19,7 @@ export function useMiniWindowSetup(alwaysOnTop: boolean, initialQueueOpen: boole
|
||||
useEffect(() => {
|
||||
if (!IS_LINUX) return;
|
||||
const apply = () => {
|
||||
invoke('set_linux_webkit_smooth_scrolling', {
|
||||
setLinuxWebkitSmoothScrolling({
|
||||
enabled: useAuthStore.getState().linuxWebkitKineticScroll,
|
||||
}).catch(() => {});
|
||||
};
|
||||
@@ -30,7 +31,7 @@ export function useMiniWindowSetup(alwaysOnTop: boolean, initialQueueOpen: boole
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialQueueOpen) return;
|
||||
invoke('resize_mini_player', {
|
||||
resizeMiniPlayer({
|
||||
width: EXPANDED_SIZE.w,
|
||||
height: readStoredExpandedHeight(),
|
||||
minWidth: EXPANDED_MIN.w,
|
||||
@@ -40,10 +41,10 @@ export function useMiniWindowSetup(alwaysOnTop: boolean, initialQueueOpen: boole
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
invoke('set_mini_player_always_on_top', { onTop: alwaysOnTop }).catch(() => {});
|
||||
setMiniPlayerAlwaysOnTop({ onTop: alwaysOnTop }).catch(() => {});
|
||||
const reapply = () => {
|
||||
if (alwaysOnTop) {
|
||||
invoke('set_mini_player_always_on_top', { onTop: true }).catch(() => {});
|
||||
setMiniPlayerAlwaysOnTop({ onTop: true }).catch(() => {});
|
||||
}
|
||||
};
|
||||
window.addEventListener('focus', reapply);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CoverArtImage } from '@/cover/CoverArtImage';
|
||||
import type { CoverArtRef } from '@/cover/types';
|
||||
import type { ArtistStats, TrackStats } from '@/music-network';
|
||||
import type { SubsonicOpenArtistRef } from '@/lib/api/subsonicTypes';
|
||||
import { OpenArtistRefInline } from '@/features/artist';
|
||||
import { OpenArtistRefInline } from '@/ui/OpenArtistRefInline';
|
||||
import { formatTrackTime } from '@/lib/format/formatDuration';
|
||||
import { renderPresetIcon, useEnrichmentPrimaryIcon, useEnrichmentPrimaryLabel } from '@/music-network';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { useCachedUrl } from '@/ui/CachedImage';
|
||||
import { OpenArtistRefInline } from '@/features/artist';
|
||||
import { OpenArtistRefInline } from '@/ui/OpenArtistRefInline';
|
||||
import { formatTrackTime } from '@/lib/format/formatDuration';
|
||||
import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { Play, HardDriveDownload, Trash2, ListPlus, ListMusic, Heart } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useOfflineStore } from '@/features/offline/store/offlineStore';
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
import { shuffleArray } from '@/lib/util/shuffleArray';
|
||||
import { getMediaDir } from '@/lib/media/mediaDir';
|
||||
import { getMediaTierSize } from '@/lib/api/syncfs';
|
||||
import { canonicalQueueServerKey, resolveIndexKey } from '@/lib/server/serverIndexKey';
|
||||
import { reconcileAllLibraryTiersFromDisk } from '@/features/offline/utils/libraryTierReconcile';
|
||||
import {
|
||||
@@ -100,8 +100,8 @@ export default function OfflineLibrary() {
|
||||
const refreshOfflineDiskSizes = useCallback(async () => {
|
||||
const mediaDir = getMediaDir();
|
||||
const [library, favorites] = await Promise.all([
|
||||
invoke<number>('get_media_tier_size', { tier: 'library', mediaDir }).catch(() => 0),
|
||||
invoke<number>('get_media_tier_size', { tier: 'favorites', mediaDir }).catch(() => 0),
|
||||
getMediaTierSize({ tier: 'library', mediaDir }).catch(() => 0),
|
||||
getMediaTierSize({ tier: 'favorites', mediaDir }).catch(() => 0),
|
||||
]);
|
||||
setOfflineDiskBytes({ library, favorites });
|
||||
}, []);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { cancelOfflineDownloads } from '@/lib/api/syncfs';
|
||||
|
||||
export interface DownloadJob {
|
||||
trackId: string;
|
||||
@@ -40,7 +40,7 @@ export const cancelledDownloads = new Set<string>();
|
||||
function abortDownloadsInRust(jobs: DownloadJob[]) {
|
||||
const downloadIds = [...new Set(jobs.map(j => j.downloadId).filter(Boolean))];
|
||||
if (downloadIds.length > 0) {
|
||||
invoke('cancel_offline_downloads', { downloadIds }).catch(() => {});
|
||||
cancelOfflineDownloads({ downloadIds }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { showToast } from '@/lib/dom/toast';
|
||||
import { useOfflineJobStore, cancelledDownloads } from '@/features/offline/store/offlineJobStore';
|
||||
import { useLocalPlaybackStore, type PinSource } from '@/store/localPlaybackStore';
|
||||
import { getMediaDir } from '@/lib/media/mediaDir';
|
||||
import { checkDirAccessible, clearOfflineCancel } from '@/lib/api/syncfs';
|
||||
import { findLocalPlaybackEntry } from '@/store/localPlaybackResolve';
|
||||
import {
|
||||
isOfflinePinComplete,
|
||||
@@ -98,7 +99,7 @@ async function runOfflinePinDownload(task: OfflinePinTask): Promise<void> {
|
||||
const mediaDir = getMediaDir();
|
||||
|
||||
if (mediaDir) {
|
||||
const ok = await invoke<boolean>('check_dir_accessible', { path: mediaDir }).catch(() => false);
|
||||
const ok = await checkDirAccessible({ path: mediaDir }).catch(() => false);
|
||||
if (!ok) {
|
||||
showToast('Speichermedium nicht gefunden. Bitte Verzeichnis in den Einstellungen prüfen.', 6000, 'error');
|
||||
return;
|
||||
@@ -162,7 +163,7 @@ async function runOfflinePinDownload(task: OfflinePinTask): Promise<void> {
|
||||
if (cancelledDownloads.has(albumId)) {
|
||||
cancelledDownloads.delete(albumId);
|
||||
jobStore.setState(state => ({ jobs: state.jobs.filter(j => j.albumId !== albumId) }));
|
||||
invoke('clear_offline_cancel', { downloadId }).catch(() => {});
|
||||
clearOfflineCancel({ downloadId }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -241,7 +242,7 @@ async function runOfflinePinDownload(task: OfflinePinTask): Promise<void> {
|
||||
}));
|
||||
}
|
||||
|
||||
invoke('clear_offline_cancel', { downloadId }).catch(() => {});
|
||||
clearOfflineCancel({ downloadId }).catch(() => {});
|
||||
setTimeout(() => {
|
||||
jobStore.setState(state => ({
|
||||
jobs: state.jobs.filter(
|
||||
|
||||
@@ -12,6 +12,12 @@ import { cancelledDownloads, useOfflineJobStore } from '@/features/offline/store
|
||||
import { useFavoritesOfflineSyncStore } from '@/features/offline/store/favoritesOfflineSyncStore';
|
||||
import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
|
||||
import { getMediaDir } from '@/lib/media/mediaDir';
|
||||
import {
|
||||
cancelOfflineDownloads,
|
||||
clearOfflineCancel,
|
||||
deleteMediaFile,
|
||||
pruneEmptyMediaTierDirs,
|
||||
} from '@/lib/api/syncfs';
|
||||
import { resolveIndexKey, serverIndexKeyForProfile } from '@/lib/server/serverIndexKey';
|
||||
import { FAVORITES_OFFLINE_JOB_ID } from '@/features/offline/utils/favoritesOfflineConstants';
|
||||
import { isActiveServerReachable } from '@/lib/network/activeServerReachability';
|
||||
@@ -49,9 +55,9 @@ function cancelInFlightFavoritesDownloads(): void {
|
||||
cancelledDownloads.add(FAVORITES_OFFLINE_JOB_ID);
|
||||
const downloadIds = rustDownloadIdsForFavoritesJobs();
|
||||
if (downloadIds.length > 0) {
|
||||
invoke('cancel_offline_downloads', { downloadIds }).catch(() => {});
|
||||
cancelOfflineDownloads({ downloadIds }).catch(() => {});
|
||||
for (const id of downloadIds) {
|
||||
invoke('clear_offline_cancel', { downloadId: id }).catch(() => {});
|
||||
clearOfflineCancel({ downloadId: id }).catch(() => {});
|
||||
}
|
||||
}
|
||||
activeFavoritesDownloadId = null;
|
||||
@@ -149,10 +155,10 @@ async function pruneOrphanFavoriteAuto(
|
||||
if (entry.tier !== 'favorite-auto') continue;
|
||||
if (!entryBelongsToServer(entry, serverId)) continue;
|
||||
if (targetIds.has(entry.trackId)) continue;
|
||||
await invoke('delete_media_file', { localPath: entry.localPath, mediaDir }).catch(() => {});
|
||||
await deleteMediaFile({ localPath: entry.localPath, mediaDir }).catch(() => {});
|
||||
lp.removeEntry(entry.trackId, entry.serverIndexKey, 'favorite-unstar-prune');
|
||||
}
|
||||
await invoke('prune_empty_media_tier_dirs', { tier: 'favorite-auto', mediaDir }).catch(() => {});
|
||||
await pruneEmptyMediaTierDirs({ tier: 'favorite-auto', mediaDir }).catch(() => {});
|
||||
}
|
||||
|
||||
export async function disableFavoritesOfflineSync(): Promise<void> {
|
||||
@@ -284,8 +290,8 @@ async function runFavoritesOfflineSyncOneServer(serverId: string, token: number)
|
||||
jobStore.setState(state => ({
|
||||
jobs: state.jobs.filter(j => j.albumId !== FAVORITES_OFFLINE_JOB_ID),
|
||||
}));
|
||||
invoke('cancel_offline_downloads', { downloadIds: [downloadId] }).catch(() => {});
|
||||
invoke('clear_offline_cancel', { downloadId }).catch(() => {});
|
||||
cancelOfflineDownloads({ downloadIds: [downloadId] }).catch(() => {});
|
||||
clearOfflineCancel({ downloadId }).catch(() => {});
|
||||
activeFavoritesDownloadId = null;
|
||||
return;
|
||||
}
|
||||
@@ -329,7 +335,7 @@ async function runFavoritesOfflineSyncOneServer(serverId: string, token: number)
|
||||
|| cancelledDownloads.has(FAVORITES_OFFLINE_JOB_ID)
|
||||
|| !targetIds.has(song.id)
|
||||
) {
|
||||
await invoke('delete_media_file', { localPath: res.path, mediaDir }).catch(() => {});
|
||||
await deleteMediaFile({ localPath: res.path, mediaDir }).catch(() => {});
|
||||
return { song, error: 'CANCELLED' };
|
||||
}
|
||||
useLocalPlaybackStore.getState().upsertEntry({
|
||||
@@ -371,10 +377,10 @@ async function runFavoritesOfflineSyncOneServer(serverId: string, token: number)
|
||||
),
|
||||
}));
|
||||
if (activeFavoritesDownloadId === downloadId) {
|
||||
invoke('clear_offline_cancel', { downloadId }).catch(() => {});
|
||||
clearOfflineCancel({ downloadId }).catch(() => {});
|
||||
activeFavoritesDownloadId = null;
|
||||
}
|
||||
await invoke('prune_empty_media_tier_dirs', { tier: 'favorite-auto', mediaDir }).catch(() => {});
|
||||
await pruneEmptyMediaTierDirs({ tier: 'favorite-auto', mediaDir }).catch(() => {});
|
||||
}
|
||||
} catch (err) {
|
||||
if (token === runToken) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { frontendDebugLog } from '@/lib/api/debugLog';
|
||||
import { libraryGetTracksBatch } from '@/lib/api/library';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useOfflineStore, type OfflineAlbumMeta } from '@/features/offline/store/offlineStore';
|
||||
@@ -6,18 +6,10 @@ import { useLocalPlaybackStore, type LocalPlaybackEntry, type PinSource } from '
|
||||
import { localPlaybackEntryKey } from '@/store/localPlaybackKeys';
|
||||
import { importLegacyLocalPlayback } from '@/store/localPlaybackMigration';
|
||||
import { getMediaDir } from '@/lib/media/mediaDir';
|
||||
import { migrateLegacyOfflineDisk } from '@/lib/api/syncfs';
|
||||
import { resolveServerIdForIndexKey } from '@/lib/server/serverLookup';
|
||||
import { resolveIndexKey } from '@/lib/server/serverIndexKey';
|
||||
|
||||
interface LegacyOfflineMigrationResult {
|
||||
trackId: string;
|
||||
serverIndexKey: string;
|
||||
path: string;
|
||||
size: number;
|
||||
layoutFingerprint: string;
|
||||
relocated: boolean;
|
||||
skippedReason?: string | null;
|
||||
}
|
||||
import type { LegacyOfflineMigrationResult } from '@/generated/bindings';
|
||||
|
||||
type PersistCapableStore = {
|
||||
persist: {
|
||||
@@ -44,10 +36,7 @@ function waitForStoreHydration(store: PersistCapableStore): Promise<void> {
|
||||
|
||||
function migrationDebug(payload: Record<string, unknown>): void {
|
||||
if (useAuthStore.getState().loggingMode !== 'debug') return;
|
||||
void invoke('frontend_debug_log', {
|
||||
scope: 'legacy-offline-migration',
|
||||
message: JSON.stringify(payload),
|
||||
}).catch(() => {});
|
||||
frontendDebugLog('legacy-offline-migration', JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function resolveIndexKeyForServerId(serverId: string): string {
|
||||
@@ -192,7 +181,7 @@ export async function runLegacyOfflineFileMigration(serverIndexKey?: string): Pr
|
||||
const customOfflineDir = useAuthStore.getState().offlineDownloadDir?.trim() || null;
|
||||
let relocated = 0;
|
||||
try {
|
||||
const results = await invoke<LegacyOfflineMigrationResult[]>('migrate_legacy_offline_disk', {
|
||||
const results = await migrateLegacyOfflineDisk({
|
||||
mediaDir: getMediaDir(),
|
||||
customOfflineDir,
|
||||
serverIndexKeyFilter: serverIndexKey ?? null,
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { libraryUpsertSongsFromApi } from '@/lib/api/library';
|
||||
import { librarySqlServerId } from '@/lib/api/coverCache';
|
||||
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import type { LocalPlaybackEntry, PinSource } from '@/store/localPlaybackStore';
|
||||
import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
|
||||
import { getMediaDir } from '@/lib/media/mediaDir';
|
||||
import { discoverLibraryTierOnDisk, pruneOrphanLibraryTierFiles } from '@/lib/api/syncfs';
|
||||
import { resolveIndexKey, serverIndexKeyForProfile } from '@/lib/server/serverIndexKey';
|
||||
import {
|
||||
entryBelongsToServer,
|
||||
findLocalPlaybackEntry,
|
||||
indexKeyBelongsToServer,
|
||||
} from '@/store/localPlaybackResolve';
|
||||
import type { LibraryTierDiskHit } from '@/generated/bindings';
|
||||
|
||||
interface LibraryTrackProbeResult {
|
||||
path: string;
|
||||
@@ -20,14 +21,6 @@ interface LibraryTrackProbeResult {
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
interface LibraryTierDiskHit {
|
||||
trackId: string;
|
||||
path: string;
|
||||
size: number;
|
||||
layoutFingerprint: string;
|
||||
suffix: string;
|
||||
}
|
||||
|
||||
export interface LibraryTierReconcileResult {
|
||||
syncedFromDisk: number;
|
||||
removedStaleIndex: number;
|
||||
@@ -93,7 +86,7 @@ async function discoverLibraryTierHits(
|
||||
const serverIndexKey = serverIndexKeyForServerId(serverId);
|
||||
const libraryServerId = librarySqlServerId(serverId);
|
||||
try {
|
||||
return await invoke<LibraryTierDiskHit[]>('discover_library_tier_on_disk', {
|
||||
return await discoverLibraryTierOnDisk({
|
||||
serverIndexKey,
|
||||
libraryServerId,
|
||||
candidateTrackIds,
|
||||
@@ -186,7 +179,7 @@ export async function reconcileLibraryTierForServer(
|
||||
|
||||
let orphansRemoved: number;
|
||||
try {
|
||||
const removed = await invoke<string[]>('prune_orphan_library_tier_files', {
|
||||
const removed = await pruneOrphanLibraryTierFiles({
|
||||
serverIndexKey,
|
||||
keepPaths: [...keepPaths],
|
||||
mediaDir: getMediaDir(),
|
||||
@@ -258,7 +251,7 @@ export async function reconcileLibraryTierForAlbum(
|
||||
|
||||
let orphansRemoved: number;
|
||||
try {
|
||||
const removed = await invoke<string[]>('prune_orphan_library_tier_files', {
|
||||
const removed = await pruneOrphanLibraryTierFiles({
|
||||
serverIndexKey,
|
||||
keepPaths: [...keepPaths],
|
||||
mediaDir: getMediaDir(),
|
||||
|
||||
@@ -3,7 +3,6 @@ import { getAlbumForServer, filterSongsToServerLibrary } from '@/lib/api/subsoni
|
||||
import { getPlaylistForServer } from '@/lib/api/subsonicPlaylists';
|
||||
import { getArtistForServer } from '@/lib/api/subsonicArtists';
|
||||
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import type { PinSource } from '@/store/localPlaybackStore';
|
||||
import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
|
||||
@@ -11,6 +10,7 @@ import { useOfflineStore } from '@/features/offline/store/offlineStore';
|
||||
import { usePlaylistStore } from '@/features/playlist';
|
||||
import { isSmartPlaylistName } from '@/lib/format/playlistDetailHelpers';
|
||||
import { getMediaDir } from '@/lib/media/mediaDir';
|
||||
import { deleteMediaFile } from '@/lib/api/syncfs';
|
||||
import {
|
||||
isActiveServerReachable,
|
||||
onActiveServerBecameReachable,
|
||||
@@ -123,7 +123,7 @@ async function pruneRemovedPinTracks(
|
||||
if (!entry?.localPath || entry.tier !== 'library') continue;
|
||||
if (entry.pinSource?.kind !== kind || entry.pinSource.sourceId !== sourceId) continue;
|
||||
|
||||
await invoke('delete_media_file', { localPath: entry.localPath, mediaDir }).catch(() => {});
|
||||
await deleteMediaFile({ localPath: entry.localPath, mediaDir }).catch(() => {});
|
||||
lp.removeEntry(trackId, entry.serverIndexKey, `${kind}-sync-prune`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* logging on in Settings, so the same data lands in `psysonic-logs-*.log`
|
||||
* for power users who want a persistent file.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { frontendDebugLog } from '@/lib/api/debugLog';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
|
||||
/** Hard cap so a long session doesn't spend memory unbounded. ~200 entries. */
|
||||
@@ -54,10 +54,7 @@ export function pushOrbitEvent(scope: string, message: string | Record<string, u
|
||||
// Bridge to the existing Rust log buffer when Debug mode is on, so
|
||||
// Settings → Debug → Export still works for the same data.
|
||||
if (useAuthStore.getState().loggingMode === 'debug') {
|
||||
void invoke('frontend_debug_log', {
|
||||
scope: `orbit:${scope}`,
|
||||
message: text,
|
||||
}).catch(() => { /* best-effort */ });
|
||||
frontendDebugLog(`orbit:${scope}`, text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
SlidersVertical, X,
|
||||
PictureInPicture2, Ellipsis,
|
||||
} from 'lucide-react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { openMiniPlayer } from '@/lib/api/miniPlayer';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
@@ -276,7 +276,7 @@ export default function PlayerBar() {
|
||||
{isLayoutVisible('miniPlayer') && (
|
||||
<button
|
||||
className="player-btn player-btn-sm"
|
||||
onClick={() => invoke('open_mini_player').catch(() => {})}
|
||||
onClick={() => openMiniPlayer().catch(() => {})}
|
||||
aria-label={t('player.miniPlayer')}
|
||||
data-tooltip={t('player.miniPlayer')}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { PictureInPicture2, SlidersVertical } from 'lucide-react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { openMiniPlayer } from '@/lib/api/miniPlayer';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { PlayerVolume } from '@/features/playback/components/playerBar/PlayerVolume';
|
||||
import { PlayerPlaybackRateMenuSection } from '@/features/playback/components/playerBar/PlayerPlaybackRate';
|
||||
@@ -65,7 +65,7 @@ export function PlayerOverflowMenu({
|
||||
<button
|
||||
className="player-overflow-menu-btn"
|
||||
onClick={() => {
|
||||
invoke('open_mini_player').catch(() => {});
|
||||
openMiniPlayer().catch(() => {});
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { albumCoverRef } from '@/cover/ref';
|
||||
import { useAlbumCoverRef } from '@/cover/useLibraryCoverRef';
|
||||
import { usePlaybackTrackCoverRef } from '@/cover/useLibraryCoverRef';
|
||||
import MarqueeText from '@/ui/MarqueeText';
|
||||
import { OpenArtistRefInline } from '@/features/artist';
|
||||
import { OpenArtistRefInline } from '@/ui/OpenArtistRefInline';
|
||||
import StarRating from '@/ui/StarRating';
|
||||
import { PlaybackBufferingOverlay } from '@/features/playback/components/PlaybackBufferingOverlay';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Blend, Moon, Pause, Play, Repeat, Repeat1, SkipBack, SkipForward, Square, Sunrise } from 'lucide-react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioPreviewStop, audioPreviewStopSilent } from '@/lib/api/audio';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
|
||||
import { useAutodjTransitionUi } from '@/features/playback/store/autodjTransitionUi';
|
||||
@@ -44,7 +44,7 @@ export function PlayerTransportControls({
|
||||
onClick={() => {
|
||||
if (isPreviewing) {
|
||||
usePreviewStore.setState({ previewingId: null, previewingTrack: null, elapsed: 0 });
|
||||
invoke('audio_preview_stop_silent').catch(() => {});
|
||||
audioPreviewStopSilent().catch(() => {});
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
@@ -86,7 +86,7 @@ export function PlayerTransportControls({
|
||||
// button — preview ends, main playback auto-resumes if it was
|
||||
// playing before. Use regular audio_preview_stop (not _silent).
|
||||
usePreviewStore.setState({ previewingId: null, previewingTrack: null, elapsed: 0 });
|
||||
invoke('audio_preview_stop').catch(() => {});
|
||||
audioPreviewStop().catch(() => {});
|
||||
})
|
||||
: playPauseBind.onClick}
|
||||
aria-label={isPreviewing
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import {
|
||||
audioCanonicalizeSelectedDevice,
|
||||
audioDefaultOutputDeviceName,
|
||||
audioListDevices,
|
||||
} from '@/lib/api/audio';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { IS_MACOS } from '@/lib/util/platform';
|
||||
@@ -35,23 +39,23 @@ export function useAudioDevicesProbe(t: TFunction): UseAudioDevicesProbeResult {
|
||||
const refreshAudioDevices = useCallback((opts?: { silent?: boolean }) => {
|
||||
const silent = !!opts?.silent;
|
||||
if (!silent) setDevicesLoading(true);
|
||||
const listP = invoke<string[]>('audio_list_devices').catch((e) => {
|
||||
const listP = audioListDevices().catch((e) => {
|
||||
console.error(e);
|
||||
showToast(t('settings.audioOutputDeviceListError'), 5000, 'error');
|
||||
return [] as string[];
|
||||
});
|
||||
const defP = invoke<string | null>('audio_default_output_device_name').catch(() => null);
|
||||
const defP = audioDefaultOutputDeviceName().catch(() => null);
|
||||
Promise.all([listP, defP])
|
||||
.then(async ([devices, osDefault]) => {
|
||||
let canon: string | null = null;
|
||||
try {
|
||||
canon = await invoke<string | null>('audio_canonicalize_selected_device');
|
||||
canon = await audioCanonicalizeSelectedDevice();
|
||||
if (canon) useAuthStore.getState().setAudioOutputDevice(canon);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const finalList = canon
|
||||
? await invoke<string[]>('audio_list_devices').catch(() => devices)
|
||||
? await audioListDevices().catch(() => devices)
|
||||
: devices;
|
||||
const defId = osDefault ?? null;
|
||||
setAudioDevices(sortAudioDeviceIds(finalList, defId));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { playbackReportStart } from '@/features/playback/store/playbackReportSession';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioStop } from '@/lib/api/audio';
|
||||
import { getPlaybackServerId } from '@/features/playback/utils/playback/playbackServer';
|
||||
import { getPlaybackSourceKind } from '@/features/playback/utils/playback/resolvePlaybackUrl';
|
||||
import {
|
||||
@@ -208,7 +208,7 @@ export function applyQueueHistorySnapshot(
|
||||
});
|
||||
|
||||
if (!nextTrack) {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
audioStop().catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
syncUserQueueMutationToServer(nextItems, null, 0);
|
||||
if (typeof snap.queueListScrollTop === 'number' && Number.isFinite(snap.queueListScrollTop)) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
playbackReportStopped,
|
||||
} from '@/features/playback/store/playbackReportSession';
|
||||
import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioPreload, audioSetAutodjSuppress } from '@/lib/api/audio';
|
||||
import { getMusicNetworkRuntimeOrNull } from '@/music-network';
|
||||
import { setDeferHotCachePrefetch } from '@/lib/cache/hotCacheGate';
|
||||
import { notifyLibraryPlaybackHint } from '@/features/playback/store/libraryPlaybackHint';
|
||||
@@ -106,7 +106,7 @@ let autodjSuppressSent: boolean | null = null;
|
||||
function syncAutodjSuppress(want: boolean): void {
|
||||
if (autodjSuppressSent === want) return;
|
||||
autodjSuppressSent = want;
|
||||
invoke('audio_set_autodj_suppress', { enabled: want }).catch(() => {});
|
||||
audioSetAutodjSuppress({ enabled: want }).catch(() => {});
|
||||
}
|
||||
|
||||
/** Rust-side `audio:normalization-state` event payload. */
|
||||
@@ -412,7 +412,7 @@ export function handleAudioProgress(
|
||||
gaplessEnabled,
|
||||
});
|
||||
}
|
||||
invoke('audio_preload', {
|
||||
audioPreload({
|
||||
url: nextUrl,
|
||||
durationHint: nextTrack.duration,
|
||||
analysisTrackId: nextTrack.id,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioSetCrossfade, audioSetGapless } from '@/lib/api/audio';
|
||||
import { effectiveLoudnessPreAnalysisAttenuationDb } from '@/lib/audio/loudnessPreAnalysisSlider';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { onAnalysisStorageChanged } from '@/store/analysisSync';
|
||||
@@ -22,11 +22,11 @@ export function setupAuthSync(): () => void {
|
||||
let prevNormTarget = normCfg.loudnessTargetLufs;
|
||||
let prevPreAnalysis = normCfg.loudnessPreAnalysisAttenuationDb;
|
||||
const unsubAuth = useAuthStore.subscribe((state) => {
|
||||
invoke('audio_set_crossfade', {
|
||||
audioSetCrossfade({
|
||||
enabled: state.crossfadeEnabled,
|
||||
secs: state.crossfadeSecs,
|
||||
}).catch(() => {});
|
||||
invoke('audio_set_gapless', { enabled: state.gaplessEnabled }).catch(() => {});
|
||||
audioSetGapless({ enabled: state.gaplessEnabled }).catch(() => {});
|
||||
const normChanged =
|
||||
state.normalizationEngine !== prevNormEngine
|
||||
|| state.loudnessTargetLufs !== prevNormTarget
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { commands } from '@/generated/bindings';
|
||||
import { resolvePlaybackCoverScope } from '@/cover/ref';
|
||||
import { resolveTrackCoverRefFromLibrary } from '@/cover/resolveEntryLibrary';
|
||||
import { coverArtUrlForDiscord } from '@/cover/integrations/discord';
|
||||
@@ -42,7 +43,7 @@ export function setupDiscordPresence(): () => void {
|
||||
discordPrevTemplateState = null;
|
||||
discordPrevTemplateLargeText = null;
|
||||
discordPrevTemplateName = null;
|
||||
invoke('discord_clear_presence').catch(() => {});
|
||||
commands.discordClearPresence().catch(() => {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioSetCrossfade, audioSetDevice, audioSetGapless, audioSetVolume } from '@/lib/api/audio';
|
||||
import { effectiveLoudnessPreAnalysisAttenuationDb } from '@/lib/audio/loudnessPreAnalysisSlider';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { emitNormalizationDebug } from '@/features/playback/store/normalizationDebug';
|
||||
@@ -18,9 +18,9 @@ export function runInitialAudioSync(): void {
|
||||
// Initial sync of audio settings to Rust engine on startup.
|
||||
const { crossfadeEnabled, crossfadeSecs, gaplessEnabled, audioOutputDevice } = useAuthStore.getState();
|
||||
const { volume } = usePlayerStore.getState();
|
||||
invoke('audio_set_volume', { volume }).catch(() => {});
|
||||
invoke('audio_set_crossfade', { enabled: crossfadeEnabled, secs: crossfadeSecs }).catch(() => {});
|
||||
invoke('audio_set_gapless', { enabled: gaplessEnabled }).catch(() => {});
|
||||
audioSetVolume({ volume }).catch(() => {});
|
||||
audioSetCrossfade({ enabled: crossfadeEnabled, secs: crossfadeSecs }).catch(() => {});
|
||||
audioSetGapless({ enabled: gaplessEnabled }).catch(() => {});
|
||||
const normCfg = useAuthStore.getState();
|
||||
usePlayerStore.setState({
|
||||
normalizationEngineLive: normCfg.normalizationEngine,
|
||||
@@ -54,6 +54,6 @@ export function runInitialAudioSync(): void {
|
||||
}
|
||||
}
|
||||
if (audioOutputDevice) {
|
||||
invoke('audio_set_device', { deviceName: audioOutputDevice }).catch(() => {});
|
||||
audioSetDevice({ deviceName: audioOutputDevice }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { mprisSetMetadata, mprisSetPlayback } from '@/lib/api/mpris';
|
||||
import { resolvePlaybackCoverScope } from '@/cover/ref';
|
||||
import { resolveTrackCoverRefFromLibrary } from '@/cover/resolveEntryLibrary';
|
||||
import { coverArtUrlForMpris } from '@/cover/integrations/mpris';
|
||||
@@ -39,7 +40,7 @@ export function setupMprisSync(): () => void {
|
||||
).then(ref => {
|
||||
if (!ref) return;
|
||||
coverArtUrlForMpris(ref)
|
||||
.then(coverUrl => invoke('mpris_set_metadata', {
|
||||
.then(coverUrl => mprisSetMetadata({
|
||||
title,
|
||||
artist,
|
||||
album,
|
||||
@@ -49,7 +50,7 @@ export function setupMprisSync(): () => void {
|
||||
.catch(() => {});
|
||||
});
|
||||
} else {
|
||||
invoke('mpris_set_metadata', {
|
||||
mprisSetMetadata({
|
||||
title,
|
||||
artist,
|
||||
album,
|
||||
@@ -64,7 +65,7 @@ export function setupMprisSync(): () => void {
|
||||
if (currentRadio && currentRadio.id !== prevRadioId) {
|
||||
prevRadioId = currentRadio.id;
|
||||
prevTrackId = null;
|
||||
invoke('mpris_set_metadata', {
|
||||
mprisSetMetadata({
|
||||
title: currentRadio.name,
|
||||
artist: null,
|
||||
album: null,
|
||||
@@ -80,7 +81,7 @@ export function setupMprisSync(): () => void {
|
||||
prevIsPlaying = isPlaying;
|
||||
lastMprisPositionUpdate = Date.now();
|
||||
const pos = getPlaybackProgressSnapshot().currentTime;
|
||||
invoke('mpris_set_playback', {
|
||||
mprisSetPlayback({
|
||||
playing: isPlaying,
|
||||
positionSecs: pos > 0 ? pos : null,
|
||||
}).catch(() => {});
|
||||
@@ -93,7 +94,7 @@ export function setupMprisSync(): () => void {
|
||||
if (currentRadio || !isPlaying) return;
|
||||
if (Date.now() - lastMprisPositionUpdate < 1500) return;
|
||||
lastMprisPositionUpdate = Date.now();
|
||||
invoke('mpris_set_playback', {
|
||||
mprisSetPlayback({
|
||||
playing: true,
|
||||
positionSecs: currentTime,
|
||||
}).catch(() => {});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { mprisSetMetadata } from '@/lib/api/mpris';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
|
||||
@@ -20,7 +20,7 @@ export function setupRadioMprisMetadata(): () => void {
|
||||
const artist = sep !== -1 ? payload.title.slice(0, sep).trim() : null;
|
||||
const title = sep !== -1 ? payload.title.slice(sep + 3).trim() : payload.title;
|
||||
|
||||
invoke('mpris_set_metadata', {
|
||||
mprisSetMetadata({
|
||||
title: title || currentRadio.name,
|
||||
artist: artist || currentRadio.name,
|
||||
album: null,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioPreload } from '@/lib/api/audio';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { autodjMaxOverlapCapSec } from '@/lib/audio/autodjOverlapCap';
|
||||
import { computeWaveformSilence, planCrossfadeTransition } from '@/lib/waveform/waveformSilence';
|
||||
@@ -74,7 +74,7 @@ export function kickEagerCrossfadePreload(
|
||||
const url = resolvePlaybackUrl(track.id, serverId ?? undefined);
|
||||
setBytePreloadingId(track.id);
|
||||
void refreshLoudnessForTrack(track.id, { syncPlayingEngine: false });
|
||||
invoke('audio_preload', {
|
||||
audioPreload({
|
||||
url,
|
||||
durationHint: track.duration,
|
||||
analysisTrackId: track.id,
|
||||
@@ -158,7 +158,7 @@ export function maybeCrossfadeBytePreload(currentTime: number, dur: number): voi
|
||||
// Loudness cache only — never refreshWaveformForTrack(next): it writes the
|
||||
// global waveformBins and would replace the current track's seekbar.
|
||||
void refreshLoudnessForTrack(nextTrack.id, { syncPlayingEngine: false });
|
||||
invoke('audio_preload', {
|
||||
audioPreload({
|
||||
url: nextUrl,
|
||||
durationHint: nextTrack.duration,
|
||||
analysisTrackId: nextTrack.id,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Track } from '@/lib/media/trackTypes';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioPause, audioSeek } from '@/lib/api/audio';
|
||||
import { setDeferHotCachePrefetch } from '@/lib/cache/hotCacheGate';
|
||||
import {
|
||||
getPlaybackIndexKey,
|
||||
@@ -75,7 +76,7 @@ export function engineLoadTrackAtPosition(opts: {
|
||||
if (getPlayGeneration() !== generation) return;
|
||||
if (!wantPlaying) {
|
||||
if (!startPaused) {
|
||||
invoke('audio_pause').catch(console.error);
|
||||
audioPause().catch(console.error);
|
||||
}
|
||||
setIsAudioPaused(true);
|
||||
usePlayerStore.setState({ isPlaying: false });
|
||||
@@ -84,7 +85,7 @@ export function engineLoadTrackAtPosition(opts: {
|
||||
}
|
||||
};
|
||||
if (canSeek) {
|
||||
void invoke('audio_seek', { seconds: seekTo }).then(afterSeek).catch(afterSeek);
|
||||
void audioSeek({ seconds: seekTo }).then(afterSeek).catch(afterSeek);
|
||||
} else {
|
||||
afterSeek();
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { create } from 'zustand';
|
||||
import type { HotCacheEntry } from '@/features/playback/store/hotCacheStoreTypes';
|
||||
import { useLocalPlaybackStore, type LocalPlaybackEntry } from '@/store/localPlaybackStore';
|
||||
import { entryBelongsToServer } from '@/store/localPlaybackResolve';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { getMediaDir } from '@/lib/media/mediaDir';
|
||||
import { deleteMediaFile } from '@/lib/api/syncfs';
|
||||
|
||||
export type { HotCacheEntry } from '@/features/playback/store/hotCacheStoreTypes';
|
||||
/** @deprecated Use {@link LOCAL_PLAYBACK_PROTECT_AFTER_CURRENT}. */
|
||||
@@ -89,9 +89,7 @@ export const useHotCacheStore = create<HotCacheState>()(() => ({
|
||||
const lp = useLocalPlaybackStore.getState();
|
||||
const e = lp.getEntry(trackId, serverId);
|
||||
if (e?.tier === 'ephemeral' && e.localPath) {
|
||||
await invoke('delete_media_file', { localPath: e.localPath, mediaDir: getMediaDir() }).catch(
|
||||
() => {},
|
||||
);
|
||||
await deleteMediaFile({ localPath: e.localPath, mediaDir: getMediaDir() }).catch(() => {});
|
||||
lp.removeEntry(trackId, serverId, 'hot-cache-shim');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { buildStreamUrl } from '@/lib/api/subsonicStreamUrl';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { commands } from '@/generated/bindings';
|
||||
import { getPlaybackIndexKey } from '@/features/playback/utils/playback/playbackServer';
|
||||
import { redactSubsonicUrlForLog } from '@/lib/server/redactSubsonicUrl';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
@@ -72,11 +72,11 @@ async function runRefreshLoudnessForTrack(trackId: string, syncEngine: boolean):
|
||||
try {
|
||||
const requestedTarget = useAuthStore.getState().loudnessTargetLufs;
|
||||
const serverId = getPlaybackIndexKey() || null;
|
||||
const row = await invoke<LoudnessCachePayload | null>('analysis_get_loudness_for_track', {
|
||||
trackId,
|
||||
targetLufs: requestedTarget,
|
||||
serverId,
|
||||
});
|
||||
const loudnessRes = await commands.analysisGetLoudnessForTrack(trackId, requestedTarget, serverId);
|
||||
if (loudnessRes.status === 'error') throw new Error(loudnessRes.error);
|
||||
// Boundary cast: the generated DTO widens `recommendedGainDb` to `number | null`;
|
||||
// downstream relies on the FE type's non-null shape (guarded at runtime by Number.isFinite).
|
||||
const row = loudnessRes.data as LoudnessCachePayload | null;
|
||||
if (useAuthStore.getState().loudnessTargetLufs !== requestedTarget) {
|
||||
emitNormalizationDebug('refresh:stale-target', { trackId, requestedTarget });
|
||||
void refreshLoudnessForTrack(trackId, { syncPlayingEngine: syncEngine });
|
||||
@@ -113,8 +113,11 @@ async function runRefreshLoudnessForTrack(trackId: string, syncEngine: boolean):
|
||||
attempt: attempts + 1,
|
||||
priority,
|
||||
});
|
||||
void invoke('analysis_enqueue_seed_from_url', { trackId, url, serverId, priority })
|
||||
.then(() => emitNormalizationDebug('backfill:queued', { trackId, attempt: attempts + 1 }))
|
||||
void commands.analysisEnqueueSeedFromUrl(trackId, url, null, serverId, priority)
|
||||
.then((res) => {
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
emitNormalizationDebug('backfill:queued', { trackId, attempt: attempts + 1 });
|
||||
})
|
||||
.catch((e) => emitNormalizationDebug('backfill:error', { trackId, error: String(e) }))
|
||||
.finally(() => {
|
||||
clearBackfillInFlight(trackId);
|
||||
|
||||
@@ -95,6 +95,8 @@ describe('reseedLoudnessForTrackId', () => {
|
||||
url: 'https://mock/stream/t1',
|
||||
force: true,
|
||||
serverId: null,
|
||||
// Typed binding threads the (unused) priority arg through; null = default (behaviour-equivalent).
|
||||
priority: null,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { buildStreamUrl } from '@/lib/api/subsonicStreamUrl';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { commands } from '@/generated/bindings';
|
||||
import { getPlaybackIndexKey } from '@/features/playback/utils/playback/playbackServer';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
@@ -45,24 +45,22 @@ export async function reseedLoudnessForTrackId(trackId: string): Promise<void> {
|
||||
});
|
||||
const serverId = getPlaybackIndexKey() || null;
|
||||
try {
|
||||
await invoke('analysis_delete_waveform_for_track', { trackId, serverId });
|
||||
const res = await commands.analysisDeleteWaveformForTrack(trackId, serverId);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
} catch (e) {
|
||||
console.error('[psysonic] analysis_delete_waveform_for_track failed:', e);
|
||||
}
|
||||
try {
|
||||
await invoke('analysis_delete_loudness_for_track', { trackId, serverId });
|
||||
const res = await commands.analysisDeleteLoudnessForTrack(trackId, serverId);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
} catch (e) {
|
||||
console.error('[psysonic] analysis_delete_loudness_for_track failed:', e);
|
||||
}
|
||||
usePlayerStore.getState().updateReplayGainForCurrentTrack();
|
||||
const url = buildStreamUrl(trackId);
|
||||
try {
|
||||
await invoke('analysis_enqueue_seed_from_url', {
|
||||
trackId,
|
||||
url,
|
||||
force: true,
|
||||
serverId,
|
||||
});
|
||||
const res = await commands.analysisEnqueueSeedFromUrl(trackId, url, true, serverId, null);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
} catch (e) {
|
||||
console.error('[psysonic] analysis_enqueue_seed_from_url (reseed) failed:', e);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { commands } from '@/generated/bindings';
|
||||
import { applyServerPlayQueue } from '@/features/playback/store/applyServerPlayQueue';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioSeek, audioSetVolume, audioStop } from '@/lib/api/audio';
|
||||
import i18n from '@/lib/i18n';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
@@ -76,10 +77,10 @@ export function createMiscActions(set: SetState, get: GetState): Pick<
|
||||
clearSeekFallbackRetry();
|
||||
clearSeekDebounce(); clearSeekTarget();
|
||||
// Stop Rust engine in case a regular track was playing.
|
||||
invoke('audio_stop').catch(() => {});
|
||||
audioStop().catch(() => {});
|
||||
// Resolve PLS/M3U playlist URLs to the actual stream URL before handing
|
||||
// to HTML5 <audio> — the browser cannot play playlist files directly.
|
||||
const streamUrl = await invoke<string>('resolve_stream_url', { url: station.streamUrl })
|
||||
const streamUrl = await commands.resolveStreamUrl(station.streamUrl)
|
||||
.catch(() => station.streamUrl);
|
||||
const { replayGainFallbackDb } = useAuthStore.getState();
|
||||
const fallbackFactor = replayGainFallbackDb !== 0 ? Math.pow(10, replayGainFallbackDb / 20) : 1;
|
||||
@@ -119,7 +120,7 @@ export function createMiscActions(set: SetState, get: GetState): Pick<
|
||||
get().playTrack(currentTrack, undefined, true);
|
||||
return;
|
||||
}
|
||||
invoke('audio_seek', { seconds: 0 }).catch(console.error);
|
||||
audioSeek({ seconds: 0 }).catch(console.error);
|
||||
set({ progress: 0, currentTime: 0 });
|
||||
return;
|
||||
}
|
||||
@@ -133,7 +134,7 @@ export function createMiscActions(set: SetState, get: GetState): Pick<
|
||||
|
||||
setVolume: (v) => {
|
||||
const clamped = Math.max(0, Math.min(1, v));
|
||||
invoke('audio_set_volume', { volume: clamped }).catch(console.error);
|
||||
audioSetVolume({ volume: clamped }).catch(console.error);
|
||||
setRadioVolume(clamped);
|
||||
set({ volume: clamped });
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getSimilarSongs2, getTopSongs } from '@/lib/api/subsonicArtists';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioStop } from '@/lib/api/audio';
|
||||
import { buildInfiniteQueueCandidates } from '@/features/playback/utils/playback/buildInfiniteQueueCandidates';
|
||||
import { songToTrack } from '@/lib/media/songToTrack';
|
||||
import { ensureQueueServerPinned } from '@/features/playback/utils/playback/playbackServer';
|
||||
@@ -58,7 +58,7 @@ function stopAtNaturalQueueEnd(set: SetState, get: GetState): void {
|
||||
if (currentTrack && queueItems.length > 0) {
|
||||
void finalizePlayQueueAtTrackEnd(queueItems, currentTrack);
|
||||
}
|
||||
invoke('audio_stop').catch(console.error);
|
||||
audioStop().catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
}
|
||||
@@ -208,7 +208,7 @@ export function runNext(set: SetState, get: GetState, manual: boolean): void {
|
||||
// Covers any active orbit phase (`active` / `joining` / `starting`)
|
||||
// so a fetch scheduled mid-join doesn't slip through.
|
||||
if (isInOrbitSession()) {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
audioStop().catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
return;
|
||||
@@ -225,7 +225,7 @@ export function runNext(set: SetState, get: GetState, manual: boolean): void {
|
||||
// The user may have joined an Orbit session while this
|
||||
// fetch was in flight — bail without touching the queue.
|
||||
if (isInOrbitSession()) {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
audioStop().catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
return;
|
||||
@@ -265,7 +265,7 @@ export function runNext(set: SetState, get: GetState, manual: boolean): void {
|
||||
// The user may have joined an Orbit session while this
|
||||
// fetch was in flight — bail without invoking playTrack.
|
||||
if (isInOrbitSession()) {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
audioStop().catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { frontendDebugLog } from '@/lib/api/debugLog';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
|
||||
/**
|
||||
@@ -12,8 +12,5 @@ import { useAuthStore } from '@/store/authStore';
|
||||
*/
|
||||
export function emitNormalizationDebug(step: string, details?: Record<string, unknown>): void {
|
||||
if (useAuthStore.getState().loggingMode !== 'debug') return;
|
||||
void invoke('frontend_debug_log', {
|
||||
scope: 'normalization',
|
||||
message: JSON.stringify({ step, details }),
|
||||
}).catch(() => {});
|
||||
frontendDebugLog('normalization', JSON.stringify({ step, details }));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioSetNormalization, audioUpdateReplayGain } from '@/lib/api/audio';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { effectiveLoudnessPreAnalysisAttenuationDb } from '@/lib/audio/loudnessPreAnalysisSlider';
|
||||
|
||||
@@ -33,7 +33,7 @@ export function invokeAudioSetNormalizationDeduped(payload: {
|
||||
}
|
||||
lastNormAudioInvokeKey = key;
|
||||
lastNormAudioInvokeAtMs = now;
|
||||
void invoke('audio_set_normalization', payload).catch(() => {});
|
||||
void audioSetNormalization(payload).catch(() => {});
|
||||
}
|
||||
|
||||
let lastRgInvokeKey = '';
|
||||
@@ -80,7 +80,7 @@ export function invokeAudioUpdateReplayGainDeduped(payload: {
|
||||
}
|
||||
lastRgInvokeKey = key;
|
||||
lastRgInvokeAtMs = now;
|
||||
invoke('audio_update_replay_gain', payload).catch(console.error);
|
||||
audioUpdateReplayGain(payload).catch(console.error);
|
||||
}
|
||||
|
||||
/** Test-only: clear the cached dedupe state so each spec starts fresh. */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { playbackReportStart, playbackReportStopped } from '@/features/playback/store/playbackReportSession';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioSeek } from '@/lib/api/audio';
|
||||
import { getMusicNetworkRuntimeOrNull } from '@/music-network';
|
||||
import { setDeferHotCachePrefetch } from '@/lib/cache/hotCacheGate';
|
||||
import { orbitBulkGuard, orbitSnapshot } from '@/store/orbitRuntime';
|
||||
@@ -524,7 +525,7 @@ export function runPlayTrack(
|
||||
const canSeekAfterPlay =
|
||||
seekTo > 0.05 && (durSeek == null || seekTo < durSeek - 0.05);
|
||||
if (canSeekAfterPlay) {
|
||||
void invoke('audio_seek', { seconds: seekTo })
|
||||
void audioSeek({ seconds: seekTo })
|
||||
.then(() => {
|
||||
if (getPlayGeneration() !== gen) return;
|
||||
setSeekTarget(seekTo);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioSetPlaybackRate } from '@/lib/api/audio';
|
||||
import {
|
||||
clampPlaybackPitch,
|
||||
clampPlaybackSpeed,
|
||||
@@ -32,7 +32,7 @@ interface PlaybackRateState extends PlaybackRateSnapshot {
|
||||
function syncPlaybackRate(state: PlaybackRateSnapshot, prev?: PlaybackRateSnapshot) {
|
||||
// Orbit sync assumes 1.0× wall-clock playback; suppress DSP without mutating prefs.
|
||||
const effectiveEnabled = state.enabled && !isOrbitPlaybackSyncActive();
|
||||
invoke('audio_set_playback_rate', {
|
||||
audioSetPlaybackRate({
|
||||
enabled: effectiveEnabled,
|
||||
strategy: engineStrategy(state.strategy),
|
||||
speed: state.speed,
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
* Side-effect wiring: keep the Rust preview sink volume aligned with the main
|
||||
* player slider while a track preview is active.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioPreviewSetVolume } from '@/lib/api/audio';
|
||||
import { computePreviewVolume, usePreviewStore } from '@/features/playback/store/previewStore';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
|
||||
usePlayerStore.subscribe((state, prev) => {
|
||||
if (state.volume === prev.volume) return;
|
||||
if (!usePreviewStore.getState().previewingId) return;
|
||||
invoke('audio_preview_set_volume', { volume: computePreviewVolume() }).catch(() => {});
|
||||
audioPreviewSetVolume({ volume: computePreviewVolume() }).catch(() => {});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { buildStreamUrl } from '@/lib/api/subsonicStreamUrl';
|
||||
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import type { TrackPreviewLocation } from '@/store/authStoreTypes';
|
||||
import { create } from 'zustand';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioPreviewPlay, audioPreviewStop } from '@/lib/api/audio';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { isOrbitPlaybackSyncActive } from '@/store/orbitRuntime';
|
||||
@@ -136,7 +136,7 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
|
||||
});
|
||||
|
||||
try {
|
||||
await invoke('audio_preview_play', {
|
||||
await audioPreviewPlay({
|
||||
id: song.id,
|
||||
url,
|
||||
startSec,
|
||||
@@ -156,7 +156,7 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
|
||||
stopPreview: async () => {
|
||||
if (!get().previewingId) return;
|
||||
try {
|
||||
await invoke('audio_preview_stop');
|
||||
await audioPreviewStop();
|
||||
} catch {
|
||||
/* engine will emit preview-end anyway; clear locally as fallback */
|
||||
set({ previewingId: null, previewingTrack: null, elapsed: 0, audioStarted: false });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioStop } from '@/lib/api/audio';
|
||||
import { orbitBulkGuard } from '@/store/orbitRuntime';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { setIsAudioPaused } from '@/features/playback/store/engineState';
|
||||
@@ -223,7 +223,7 @@ export function createQueueMutationActions(set: SetState, get: GetState): Pick<
|
||||
|
||||
clearQueue: () => {
|
||||
void playListenSessionFinalize('stop');
|
||||
invoke('audio_stop').catch(console.error);
|
||||
audioStop().catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
clearSeekFallbackRetry();
|
||||
clearSeekDebounce(); clearSeekTarget();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getSong } from '@/lib/api/subsonicLibrary';
|
||||
import { enrichTrackPlaybackMetadata } from '@/features/playback/utils/audio/enrichTrackReplayGainMetadata';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioResume, audioSeek } from '@/lib/api/audio';
|
||||
import { estimateLivePosition, orbitSnapshot } from '@/store/orbitRuntime';
|
||||
import { setDeferHotCachePrefetch } from '@/lib/cache/hotCacheGate';
|
||||
import {
|
||||
@@ -90,7 +91,7 @@ export function runResume(set: SetState, get: GetState): void {
|
||||
// Bypasses this resume() branch re-entry via the early return below.
|
||||
get().seek(fraction);
|
||||
if (getIsAudioPaused()) {
|
||||
invoke('audio_resume').catch(console.error);
|
||||
audioResume().catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: true });
|
||||
playbackReportPlaying(targetSec);
|
||||
@@ -128,7 +129,7 @@ export function runResume(set: SetState, get: GetState): void {
|
||||
|
||||
if (getIsAudioPaused()) {
|
||||
// Rust engine has audio loaded but paused — just resume it.
|
||||
invoke('audio_resume').catch(console.error);
|
||||
audioResume().catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: true });
|
||||
// Mirror pause(): tell the server immediately, don't wait for `audio:playing`.
|
||||
@@ -196,7 +197,7 @@ export function runResume(set: SetState, get: GetState): void {
|
||||
startPaused: false,
|
||||
}).then(() => {
|
||||
if (getPlayGeneration() === gen && currentTime > 1) {
|
||||
invoke('audio_seek', { seconds: currentTime }).catch(console.error);
|
||||
audioSeek({ seconds: currentTime }).catch(console.error);
|
||||
}
|
||||
}).catch((err: unknown) => {
|
||||
if (getPlayGeneration() !== gen) return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioSeek } from '@/lib/api/audio';
|
||||
import { playbackReportSeek } from '@/features/playback/store/playbackReportSession';
|
||||
import { isRecoverableSeekError } from '@/features/playback/utils/audio/seekErrors';
|
||||
import { getPlaybackServerId } from '@/features/playback/utils/playback/playbackServer';
|
||||
@@ -64,7 +64,7 @@ export function runSeek(set: SetState, get: GetState, progress: number): void {
|
||||
s0.playTrack(s0.currentTrack, undefined, true);
|
||||
return;
|
||||
}
|
||||
invoke('audio_seek', { seconds: time }).then(() => {
|
||||
audioSeek({ seconds: time }).then(() => {
|
||||
// Arm stale-progress guard only after backend acknowledged seek.
|
||||
setSeekTarget(time);
|
||||
noteEngineProgressForGapless(time);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioSeek } from '@/lib/api/audio';
|
||||
import { isRecoverableSeekError } from '@/features/playback/utils/audio/seekErrors';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { setSeekTarget } from '@/features/playback/store/seekTargetState';
|
||||
@@ -69,7 +69,7 @@ export function scheduleSeekFallbackRetry(trackId: string, seconds: number): voi
|
||||
seekFallbackVisualTarget = null;
|
||||
return;
|
||||
}
|
||||
invoke('audio_seek', { seconds: target.seconds }).then(() => {
|
||||
audioSeek({ seconds: target.seconds }).then(() => {
|
||||
setSeekTarget(target.seconds);
|
||||
seekFallbackVisualTarget = null;
|
||||
clearSeekFallbackRetry();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioPause, audioStop } from '@/lib/api/audio';
|
||||
import { setIsAudioPaused } from '@/features/playback/store/engineState';
|
||||
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
|
||||
import { flushQueueSyncToServer } from '@/features/playback/store/queueSync';
|
||||
@@ -43,7 +43,7 @@ export function createTransportLightActions(set: SetState, get: GetState): Pick<
|
||||
if (wasRadio) {
|
||||
stopRadio();
|
||||
} else {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
audioStop().catch(console.error);
|
||||
}
|
||||
setIsAudioPaused(false);
|
||||
clearSeekFallbackRetry();
|
||||
@@ -80,7 +80,7 @@ export function createTransportLightActions(set: SetState, get: GetState): Pick<
|
||||
if (get().currentRadio) {
|
||||
pauseRadio();
|
||||
} else {
|
||||
invoke('audio_pause').catch(console.error);
|
||||
audioPause().catch(console.error);
|
||||
setIsAudioPaused(true);
|
||||
playbackReportPaused(get().currentTime);
|
||||
// Flush position so a quick close after pause still leaves the
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { commands } from '@/generated/bindings';
|
||||
import { coerceWaveformBins } from '@/lib/waveform/waveformParse';
|
||||
import { getPlaybackIndexKey } from '@/features/playback/utils/playback/playbackServer';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
@@ -35,10 +35,9 @@ export async function fetchWaveformBins(
|
||||
): Promise<number[] | null> {
|
||||
if (!trackId) return null;
|
||||
try {
|
||||
const row = await invoke<WaveformCachePayload | null>('analysis_get_waveform_for_track', {
|
||||
trackId,
|
||||
serverId: serverId ?? getPlaybackIndexKey() ?? null,
|
||||
});
|
||||
const res = await commands.analysisGetWaveformForTrack(trackId, serverId ?? getPlaybackIndexKey() ?? null);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
const row = res.data;
|
||||
const bins = row ? coerceWaveformBins(row.bins) : null;
|
||||
return bins && bins.length > 0 ? bins : null;
|
||||
} catch {
|
||||
@@ -50,10 +49,9 @@ export async function refreshWaveformForTrack(trackId: string): Promise<void> {
|
||||
if (!trackId) return;
|
||||
const gen = getWaveformRefreshGen(trackId);
|
||||
try {
|
||||
const row = await invoke<WaveformCachePayload | null>('analysis_get_waveform_for_track', {
|
||||
trackId,
|
||||
serverId: getPlaybackIndexKey() || null,
|
||||
});
|
||||
const res = await commands.analysisGetWaveformForTrack(trackId, getPlaybackIndexKey() || null);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
const row = res.data;
|
||||
if (getWaveformRefreshGen(trackId) !== gen) return;
|
||||
// Never apply bins for a non-current track (e.g. gapless byte-preload fetches the neighbour).
|
||||
if (usePlayerStore.getState().currentTrack?.id !== trackId) return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioBeginOutgoingFade } from '@/lib/api/audio';
|
||||
import type { Track } from '@/lib/media/trackTypes';
|
||||
import {
|
||||
INTERRUPT_BLEND_PREP_FADE_SEC,
|
||||
@@ -53,7 +53,7 @@ export async function runInterruptBlendPrep(
|
||||
isStale: () => boolean,
|
||||
): Promise<InterruptBlendPrepResult> {
|
||||
kickEagerCrossfadePreload(track, profileId, cacheKey);
|
||||
void invoke('audio_begin_outgoing_fade', { fadeSecs: INTERRUPT_BLEND_PREP_FADE_SEC }).catch(() => {});
|
||||
void audioBeginOutgoingFade({ fadeSecs: INTERRUPT_BLEND_PREP_FADE_SEC }).catch(() => {});
|
||||
|
||||
const prepMs = Math.round(INTERRUPT_BLEND_PREP_FADE_SEC * 1000);
|
||||
await Promise.all([
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { downloadZip } from '@/lib/api/downloadZip';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { buildDownloadUrl } from '@/lib/api/subsonicStreamUrl';
|
||||
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
|
||||
@@ -27,7 +27,7 @@ export async function runPlaylistZipDownload(deps: RunPlaylistZipDownloadDeps):
|
||||
start(downloadId, filename);
|
||||
setZipDownloadId(downloadId);
|
||||
try {
|
||||
await invoke('download_zip', { id: downloadId, url, destPath });
|
||||
await downloadZip({ id: downloadId, url, destPath });
|
||||
complete(downloadId);
|
||||
} catch (e) {
|
||||
fail(downloadId);
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useQueueTrackEnrichment } from '@/features/queue/hooks/useQueueTrackEnr
|
||||
import { QueueLufsTargetMenu } from '@/features/queue/components/QueueLufsTargetMenu';
|
||||
import { PlaybackBufferingOverlay } from '@/features/playback/components/PlaybackBufferingOverlay';
|
||||
import { CoverArtImage } from '@/cover/CoverArtImage';
|
||||
import { OpenArtistRefInline } from '@/features/artist';
|
||||
import { OpenArtistRefInline } from '@/ui/OpenArtistRefInline';
|
||||
import { usePlaybackTrackCoverRef } from '@/cover/useLibraryCoverRef';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { resolveTrackArtistRefs } from '@/features/playback/utils/playback/trackArtistRefs';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { InternetRadioStation } from '@/lib/api/subsonicTypes';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { commands, type IcyMetadata } from '@/generated/bindings';
|
||||
import { usePerfProbeFlags } from '@/lib/perf/perfFlags';
|
||||
import {
|
||||
guessAzuraCastApiUrl,
|
||||
@@ -43,14 +43,6 @@ export interface RadioMetadata {
|
||||
|
||||
// ─── ICY metadata interface (matches Rust IcyMetadata struct) ─────────────────
|
||||
|
||||
interface IcyMetadataResult {
|
||||
stream_title?: string;
|
||||
icy_name?: string;
|
||||
icy_genre?: string;
|
||||
icy_url?: string;
|
||||
icy_description?: string;
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function parseIcyStreamTitle(streamTitle: string): { artist?: string; title: string } {
|
||||
@@ -177,13 +169,15 @@ export function useRadioMetadata(station: InternetRadioStation | null): RadioMet
|
||||
const currentStation = stationRef.current;
|
||||
if (!currentStation) return;
|
||||
try {
|
||||
const result: IcyMetadataResult = await invoke('fetch_icy_metadata', { url: currentStation.streamUrl });
|
||||
const res = await commands.fetchIcyMetadata(currentStation.streamUrl);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
const result: IcyMetadata = res.data;
|
||||
if (cancelled) return;
|
||||
if (result.stream_title || result.icy_name) {
|
||||
const parsed = result.stream_title ? parseIcyStreamTitle(result.stream_title) : null;
|
||||
setMetadata({
|
||||
source: 'icy',
|
||||
stationName: result.icy_name,
|
||||
stationName: result.icy_name ?? undefined,
|
||||
currentTitle: parsed?.title,
|
||||
currentArtist: parsed?.artist,
|
||||
history: [],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { mprisSetMetadata } from '@/lib/api/mpris';
|
||||
import type { InternetRadioStation } from '@/lib/api/subsonicTypes';
|
||||
import type { RadioMetadata } from '@/features/radio/hooks/useRadioMetadata';
|
||||
|
||||
@@ -62,7 +62,7 @@ export function useRadioMprisSync(
|
||||
}
|
||||
|
||||
// Mirror to the souvlaki-backed controls for desktops that surface those.
|
||||
invoke('mpris_set_metadata', {
|
||||
mprisSetMetadata({
|
||||
title,
|
||||
artist,
|
||||
album: album ?? null,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { filterSongsToActiveLibrary, getRandomSongs } from '@/lib/api/subsonicLi
|
||||
import type { SubsonicAlbum, SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import type { QueueItemRef } from '@/lib/media/trackTypes';
|
||||
import { songToTrack } from '@/lib/media/songToTrack';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { frontendDebugLog } from '@/lib/api/debugLog';
|
||||
import i18n from '@/lib/i18n';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { pushQueueUndoFromGetter } from '@/features/playback/store/queueUndo';
|
||||
@@ -58,10 +58,7 @@ export async function buildAndPlayLuckyMix(): Promise<void> {
|
||||
const payload = { step, details };
|
||||
debugSteps.push(payload);
|
||||
console.debug('[psysonic][lucky-mix]', payload);
|
||||
void invoke('frontend_debug_log', {
|
||||
scope: 'lucky-mix',
|
||||
message: JSON.stringify(payload),
|
||||
}).catch(() => {});
|
||||
frontendDebugLog('lucky-mix', JSON.stringify(payload));
|
||||
};
|
||||
const songDebug = (songs: SubsonicSong[]) =>
|
||||
songs.map(s => ({ id: s.id, title: s.title, artist: s.artist, rating: s.userRating ?? 0 }));
|
||||
@@ -356,10 +353,7 @@ export async function buildAndPlayLuckyMix(): Promise<void> {
|
||||
logStep('done', { queueCount: finalQueueCount });
|
||||
if (debugEnabled) {
|
||||
console.debug('[psysonic][lucky-mix] full-steps', debugSteps);
|
||||
void invoke('frontend_debug_log', {
|
||||
scope: 'lucky-mix',
|
||||
message: JSON.stringify({ step: 'full-steps', details: debugSteps }),
|
||||
}).catch(() => {});
|
||||
frontendDebugLog('lucky-mix', JSON.stringify({ step: 'full-steps', details: debugSteps }));
|
||||
}
|
||||
} catch (err) {
|
||||
// Cancellation is a user-initiated path, not an error. Silent teardown.
|
||||
@@ -367,10 +361,7 @@ export async function buildAndPlayLuckyMix(): Promise<void> {
|
||||
logStep('cancelled');
|
||||
if (debugEnabled) {
|
||||
console.debug('[psysonic][lucky-mix] full-steps', debugSteps);
|
||||
void invoke('frontend_debug_log', {
|
||||
scope: 'lucky-mix',
|
||||
message: JSON.stringify({ step: 'full-steps', details: debugSteps }),
|
||||
}).catch(() => {});
|
||||
frontendDebugLog('lucky-mix', JSON.stringify({ step: 'full-steps', details: debugSteps }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -378,10 +369,7 @@ export async function buildAndPlayLuckyMix(): Promise<void> {
|
||||
logStep('failed', { error: String(err) });
|
||||
if (debugEnabled) {
|
||||
console.debug('[psysonic][lucky-mix] full-steps', debugSteps);
|
||||
void invoke('frontend_debug_log', {
|
||||
scope: 'lucky-mix',
|
||||
message: JSON.stringify({ step: 'full-steps', details: debugSteps }),
|
||||
}).catch(() => {});
|
||||
frontendDebugLog('lucky-mix', JSON.stringify({ step: 'full-steps', details: debugSteps }));
|
||||
}
|
||||
// If we failed before ever calling playTrack, the queue-prune we did up
|
||||
// front left the user with nothing. Restore the snapshot so they land
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ndListSongs, ndInvalidateSongsCache } from '@/lib/api/navidromeBrowse';
|
||||
import { usePerfProbeFlags } from '@/lib/perf/perfFlags';
|
||||
import { useNavigateToAlbum } from '@/features/album';
|
||||
import { useNavigateToArtist } from '@/features/artist';
|
||||
import { OpenArtistRefInline } from '@/features/artist';
|
||||
import { OpenArtistRefInline } from '@/ui/OpenArtistRefInline';
|
||||
import { resolveTrackArtistRefs } from '@/features/playback/utils/playback/trackArtistRefs';
|
||||
|
||||
const RANDOM_RAIL_SIZE = 18;
|
||||
|
||||
@@ -542,7 +542,7 @@ export default function AnalyticsStrategySection() {
|
||||
>
|
||||
{(failedTracksByServer[failedModalTarget.indexKey] ?? []).map(track => (
|
||||
<div
|
||||
key={`${track.trackId}:${track.md5_16kb}:${track.updatedAt}`}
|
||||
key={`${track.trackId}:${track.md516kb}:${track.updatedAt}`}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
borderBottom: '1px solid var(--border-subtle, rgba(255,255,255,0.06))',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { isTilingWmCmd } from '@/lib/api/platformShell';
|
||||
import { LayoutGrid, Maximize2, Palette, Sliders, Type, ZoomIn } from 'lucide-react';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import {
|
||||
@@ -28,7 +28,7 @@ export function AppearanceTab() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_LINUX) return;
|
||||
invoke<boolean>('is_tiling_wm_cmd').then(setIsTilingWm).catch(() => {});
|
||||
isTilingWmCmd().then(setIsTilingWm).catch(() => {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog';
|
||||
import { Download, FolderOpen, Trash2, X } from 'lucide-react';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { countHotCacheTracks } from '@/features/playback/store/hotCacheStore';
|
||||
import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
|
||||
import { getMediaTierSize } from '@/lib/api/syncfs';
|
||||
import { formatBytes, snapHotCacheMb } from '@/lib/format/formatBytes';
|
||||
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
|
||||
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
|
||||
@@ -29,7 +29,7 @@ export function StorageTab() {
|
||||
);
|
||||
|
||||
const refreshHotCacheSize = useCallback(() => {
|
||||
invoke<number>('get_media_tier_size', { tier: 'ephemeral', mediaDir })
|
||||
getMediaTierSize({ tier: 'ephemeral', mediaDir })
|
||||
.then(setHotCacheBytes)
|
||||
.catch(() => setHotCacheBytes(0));
|
||||
}, [mediaDir]);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { commands } from '@/generated/bindings';
|
||||
import { linuxWaylandTextRenderSettingsAvailable } from '@/lib/api/platformShell';
|
||||
import { save as saveDialog } from '@tauri-apps/plugin-dialog';
|
||||
import { open as openUrl } from '@tauri-apps/plugin-shell';
|
||||
import { AppWindow, ChevronDown, Download, ExternalLink, Globe, HardDrive, Info, Scale, Sliders, Users } from 'lucide-react';
|
||||
@@ -29,7 +30,7 @@ export function SystemTab() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_LINUX) return;
|
||||
invoke<boolean>('linux_wayland_text_render_settings_available')
|
||||
linuxWaylandTextRenderSettingsAvailable()
|
||||
.then(setWaylandTextRenderAvailable)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
@@ -43,8 +44,9 @@ export function SystemTab() {
|
||||
});
|
||||
if (!selected || Array.isArray(selected)) return;
|
||||
try {
|
||||
const lines = await invoke<number>('export_runtime_logs', { path: selected });
|
||||
showToast(t('settings.loggingExportSuccess', { count: lines }), 3500, 'info');
|
||||
const res = await commands.exportRuntimeLogs(selected);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
showToast(t('settings.loggingExportSuccess', { count: res.data }), 3500, 'info');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(t('settings.loggingExportError'), 4500, 'error');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Upload, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { commands } from '@/generated/bindings';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { useInstalledThemesStore } from '@/store/installedThemesStore';
|
||||
import { validateThemePackage, type ValidatedTheme } from '@/lib/themes/validateThemePackage';
|
||||
@@ -39,7 +39,9 @@ export function ThemeImportSection() {
|
||||
|
||||
setImporting(true);
|
||||
try {
|
||||
const files = await invoke<{ manifest: string; css: string }>('import_theme_zip', { path: selected });
|
||||
const res = await commands.importThemeZip(selected);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
const files = res.data;
|
||||
const result = validateThemePackage(files.manifest, files.css);
|
||||
if (!result.ok) {
|
||||
setImportErrors(result.errors);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { audioSetDevice } from '@/lib/api/audio';
|
||||
import { AudioLines, RotateCcw } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import CustomSelect from '@/ui/CustomSelect';
|
||||
@@ -61,7 +61,7 @@ export function AudioOutputDeviceSection({
|
||||
const device = val || null;
|
||||
setDeviceSwitching(true);
|
||||
try {
|
||||
await invoke('audio_set_device', { deviceName: device });
|
||||
await audioSetDevice({ deviceName: device });
|
||||
setAudioOutputDevice(device);
|
||||
} catch { /* device open failed — don't persist */ }
|
||||
setDeviceSwitching(false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { themeAnimationRisk } from '@/lib/api/platformShell';
|
||||
import { IS_LINUX } from '@/lib/util/platform';
|
||||
|
||||
/**
|
||||
@@ -27,7 +27,7 @@ export function useThemeAnimationRisk(): boolean {
|
||||
return;
|
||||
}
|
||||
let alive = true;
|
||||
const p = invoke<boolean>('theme_animation_risk');
|
||||
const p = themeAnimationRisk();
|
||||
// Guard the mocked-in-tests case where invoke isn't a real promise.
|
||||
if (p && typeof (p as Promise<boolean>).then === 'function') {
|
||||
(p as Promise<boolean>)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { save, open as openDialog } from '@tauri-apps/plugin-dialog';
|
||||
import { writeFile, readTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { commands } from '@/generated/bindings';
|
||||
import { version as appVersion } from '@/../package.json';
|
||||
|
||||
const BACKUP_VERSION = 1;
|
||||
@@ -77,7 +78,8 @@ export async function exportBackupToPath(mode: BackupExportMode, path: string):
|
||||
return;
|
||||
}
|
||||
if (mode === 'library') {
|
||||
await invoke('backup_export_library_db', { destinationPath: path });
|
||||
const res = await commands.backupExportLibraryDb(path);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
return;
|
||||
}
|
||||
const content = JSON.stringify(buildSettingsManifest(), null, 2);
|
||||
@@ -119,7 +121,8 @@ export async function importAnyBackupFromPath(path: string): Promise<ImportedBac
|
||||
// Not a full backup archive, continue detection.
|
||||
}
|
||||
|
||||
await invoke('backup_import_library_db', { sourcePath: path });
|
||||
const res = await commands.backupImportLibraryDb(path);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
return 'databases';
|
||||
}
|
||||
|
||||
@@ -167,7 +170,8 @@ export async function importLibraryDatabaseBackup(): Promise<void> {
|
||||
title: 'Import Library Databases Archive',
|
||||
});
|
||||
if (!path || typeof path !== 'string') return;
|
||||
await invoke('backup_import_library_db', { sourcePath: path });
|
||||
const res = await commands.backupImportLibraryDb(path);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
}
|
||||
|
||||
export async function exportFullBackup(): Promise<string | null> {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createPortal } from 'react-dom';
|
||||
import { save as saveDialog } from '@tauri-apps/plugin-dialog';
|
||||
import { writeFile } from '@tauri-apps/plugin-fs';
|
||||
import { getLoggingMode, tailRuntimeLogs, type RuntimeLogLine } from '@/lib/api/runtimeLogs';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { setLoggingMode as setBackendLoggingMode } from '@/lib/api/platformShell';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import type { LoggingMode } from '@/store/authStoreTypes';
|
||||
import CustomSelect from '@/ui/CustomSelect';
|
||||
@@ -182,7 +182,7 @@ export default function SidebarPerfProbeLogsTab() {
|
||||
|
||||
const changeDepth = (mode: LoggingMode) => {
|
||||
setLoggingMode(mode);
|
||||
void invoke('set_logging_mode', { mode }).catch(() => {});
|
||||
void setBackendLoggingMode({ mode }).catch(() => {});
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { dirname } from '@tauri-apps/api/path';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { commands } from '@/generated/bindings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { version as currentVersion } from '../../../../package.json';
|
||||
import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '@/lib/util/platform';
|
||||
@@ -50,7 +50,7 @@ export function useAppUpdater() {
|
||||
assets: data.assets ?? [],
|
||||
});
|
||||
if (IS_LINUX) {
|
||||
const arch = await invoke<boolean>('check_arch_linux');
|
||||
const arch = await commands.checkArchLinux();
|
||||
setIsArch(arch);
|
||||
}
|
||||
} catch {
|
||||
@@ -172,10 +172,9 @@ export function useAppUpdater() {
|
||||
unlistenRef.current = unlisten;
|
||||
|
||||
try {
|
||||
const finalPath = await invoke<string>('download_update', {
|
||||
url: asset.browser_download_url,
|
||||
filename: asset.name,
|
||||
});
|
||||
const dlRes = await commands.downloadUpdate(asset.browser_download_url, asset.name);
|
||||
if (dlRes.status === 'error') throw new Error(dlRes.error);
|
||||
const finalPath = dlRes.data;
|
||||
unlisten();
|
||||
unlistenRef.current = null;
|
||||
setDlPath(finalPath);
|
||||
@@ -192,7 +191,8 @@ export function useAppUpdater() {
|
||||
// tauri-plugin-shell's open() only allows https:// per capability scope —
|
||||
// local paths are blocked and fail silently. Delegate to Rust instead.
|
||||
const dir = await dirname(dlPath);
|
||||
await invoke('open_folder', { path: dir });
|
||||
const res = await commands.openFolder(dir);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
};
|
||||
|
||||
const showAurHint = IS_LINUX && isArch;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { invoke, isTauri } from '@tauri-apps/api/core';
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import { commands } from '@/generated/bindings';
|
||||
|
||||
export const RELEASE_NOTES_REPO = 'Psychotoxical/psysonic';
|
||||
|
||||
@@ -19,7 +20,9 @@ export function whatsNewDownloadUrl(version: string): string {
|
||||
* (same as radio favicons and other non-CORS endpoints).
|
||||
*/
|
||||
async function fetchBytesViaRust(url: string): Promise<Uint8Array> {
|
||||
const [bytes] = await invoke<[number[], string]>('fetch_url_bytes', { url });
|
||||
const res = await commands.fetchUrlBytes(url);
|
||||
if (res.status === 'error') throw new Error(res.error);
|
||||
const [bytes] = res.data;
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user