refactor(ipc): complete tauri-specta typed-IPC cutover, contract guards, and layering cleanup (#1230)

This commit is contained in:
Psychotoxical
2026-07-04 16:00:31 +02:00
committed by GitHub
parent fdbb9deac6
commit 77ab95170d
199 changed files with 3691 additions and 1252 deletions
+1 -1
View File
@@ -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';
+2 -2
View File
@@ -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);
+2 -2
View File
@@ -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);
+2 -2
View File
@@ -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);
+2 -2
View File
@@ -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);
+2 -2
View File
@@ -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>;
}
+4 -4
View File
@@ -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,
});
+12 -8
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+4 -2
View File
@@ -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;
}
+2 -2
View File
@@ -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);
+1 -1
View File
@@ -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(() => {});
}
}
+4 -3
View File
@@ -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`);
}
}
+2 -5
View File
@@ -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();
}
+2 -4
View File
@@ -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');
}
},
+11 -8
View File
@@ -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);
}
+6 -5
View File
@@ -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 });
},
+5 -5
View File
@@ -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(() => {});
});
+3 -3
View File
@@ -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();
+4 -3
View File
@@ -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;
+2 -2
View File
@@ -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';
+5 -11
View File
@@ -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,
+5 -17
View File
@@ -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>)
+7 -3
View File
@@ -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 = () => {
+7 -7
View File
@@ -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);
}