// Thin wrapper over Tauri IPC so components never import tauri directly. import { invoke as tauriInvoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { mockInvoke } from "./mockLibrary.js"; // mockInvoke is inert (returns undefined) unless localStorage.mockLibrary === "1". const invoke = (cmd, args) => mockInvoke(cmd, args) ?? tauriInvoke(cmd, args); // Artist-page payloads are keyed by a Deezer id all the way through the // backend. Reject a mismatched response at the IPC boundary so a provider, // cache, or future command regression cannot populate another artist's page. // String comparison deliberately accepts route-string ids and numeric Tauri // ids as the same identity without coercing the u64 argument sent to Rust. const artistIdKey = (value) => (value == null ? "" : String(value).trim()); const sameArtistId = (left, right) => { const leftKey = artistIdKey(left); return !!leftKey && leftKey === artistIdKey(right); }; const checkedArtistPayload = (command, artistId) => invoke(command, { artistId }).then((payload) => { if (!sameArtistId(payload?.artist?.id, artistId)) { throw new Error(`${command} returned a mismatched artist identity`); } return payload; }); const checkedArtistIdentity = (artistId, artist) => invoke("catalog_artist_identity", { artistId, artist }).then((payload) => { if (!sameArtistId(payload?.artist_id ?? payload?.artistId, artistId)) { throw new Error("catalog_artist_identity returned a mismatched artist identity"); } return payload; }); // The id is COERCED to a number here, not merely passed along. Deezer album // ids reach the frontend as strings from two places — SQLite stores the // recently-played anchor as TEXT, and page seeds carry `String(id)` — and the // Rust command takes a u64, so a string argument is rejected outright with // "invalid type: string … expected u64". That made every streamed row in the // sidebar's history dead on click (2026-08-01). The identity check below still // compares through `sameArtistId`, which normalizes both sides, so a numeric // id and its string form remain the same album. const checkedAlbumPayload = (albumId) => { const numeric = typeof albumId === "number" ? albumId : Number(String(albumId).trim()); if (!Number.isFinite(numeric)) { return Promise.reject(new Error(`catalog_album: not an album id: ${albumId}`)); } return invoke("catalog_album", { albumId: numeric }).then((payload) => { if (!sameArtistId(payload?.album?.id, albumId)) { throw new Error("catalog_album returned a mismatched album identity"); } return payload; }); }; export const appStatus = () => invoke("app_status"); export const getCredentials = () => invoke("get_credentials"); export const connectAndLogin = (username, password, remember) => invoke("connect_and_login", { username, password, remember }); export const connectStored = () => invoke("connect_stored"); export const disconnect = () => invoke("disconnect"); export const startSearch = (query) => invoke("start_search", { query }); export const excludedSearchPhrases = () => invoke("excluded_search_phrases"); export const stopSearch = (token) => invoke("stop_search", { token }); export const downloadFile = (username, path, size) => invoke("download_file", { username, path, size }); export const copyStreamToFolder = (files, folder) => invoke("copy_stream_to_folder", { files, folder }); export const saveToLibrary = (files) => invoke("save_to_library", { files }); // Byte length of the cached copy of one peer file (null when uncached) — // compared against the peer-stated size to tell "finished" from "prefix". export const streamCacheLen = (username, path) => invoke("stream_cache_len", { username, path }); export const cancelDownload = (id) => invoke("cancel_download", { id }); export const showInFolder = (path) => invoke("show_in_folder", { path }); export const refreshArtistArt = () => invoke("refresh_artist_art"); export const playStream = (username, path, size, duration) => invoke("play_stream", { username, path, size, duration }); export const playLocal = (path, duration, startSecs = null) => invoke("play_local", { path, duration, startSecs }); export const preloadLocal = (path, duration) => invoke("preload_local", { path, duration }); export const playerControl = (action, value, rate) => invoke("player_control", { action, value: value ?? null, rate: rate ?? null }); // What the audio thread is doing right now. The webview only ever learns about // playback from events, so after a reload this is the only way to find out that // audio is still running — see `restoreSession`. export const playerState = () => invoke("player_state"); // The mini player window (see mini/ and miniBridge.svelte.js). export const miniOpen = (width, height) => invoke("mini_open", { width, height }); export const miniClose = () => invoke("mini_close"); export const waveform = (path, buckets, startSecs = null, endSecs = null) => invoke("waveform", { path, buckets, startSecs, endSecs }); export const libraryAlbums = () => invoke("library_albums"); export const libraryAlbumStats = (minSecs) => invoke("library_album_stats", { minSecs }); export const albumTracks = (albumId) => invoke("album_tracks", { albumId }); // `path` anchors the play to a library album via tracks.path (UNIQUE). Without // it the row still records, but with no album — so it cannot appear in recents. // `cat` carries catalog identity for a STREAMED play, which never touches the // local library: { albumId, albumTitle, albumArtist, coverUrl }. export const recordPlay = (artist, title, path, cat = {}) => invoke("record_play", { artist, title, path: path ?? null, catalogAlbumId: cat.albumId != null ? String(cat.albumId) : null, albumTitle: cat.albumTitle ?? null, albumArtist: cat.albumArtist ?? null, coverUrl: cat.coverUrl ?? null, }); // Destructive, not undoable: wipes `plays` only. Returns rows removed. export const clearPlayHistory = () => invoke("clear_play_history"); export const playRowCount = () => invoke("play_row_count"); export const recentAlbums = (limit, minSecs) => invoke("recent_albums", { limit: limit ?? null, minSecs: minSecs ?? null }); export const addPlaySeconds = (playId, secs) => invoke("add_play_seconds", { playId, secs }); export const artistPlayStats = (artist) => invoke("artist_play_stats", { artist }); // Followed artists — the home redesign's future "new releases by artists you // follow" reads the same list, which is why the rows carry name and portrait. export const followedArtists = () => invoke("followed_artists"); export const followArtist = (artistId, name, portrait) => invoke("follow_artist", { artistId, name, portrait: portrait ?? null }); export const unfollowArtist = (artistId) => invoke("unfollow_artist", { artistId }); // Home landing page. `homeWarm` walks the follows through the same metadata // entry points the pages use so every cache the feed reads is hot (measured // 48s cold for 9 artists — the page itself must never pay that); `homeArtistNews` // is one paced Google News query per followed artist, answered from a 6h cache. export const homeWarm = () => invoke("home_warm"); export const homeArtistNews = (artists) => invoke("home_artist_news", { artists }); // The OS media card (Windows SMTC). Only the metadata crosses IPC and only on // a track change — status and position are pushed by the backend's own player // event pump, which already has both. See src-tauri/src/smtc.rs. export const smtcMetadata = (title, artist, album, art) => invoke("smtc_metadata", { title: title ?? "", artist: artist ?? "", album: album ?? "", art: art ?? null, }); // The WebView blocks target="_blank"; external links open via the backend. export const openExternal = (url) => invoke("open_external", { url }); // ---- the Soulseek hub: rooms / chat / buddies / browse ---- export const slskJoinRoom = (room) => invoke("slsk_join_room", { room }); export const slskLeaveRoom = (room) => invoke("slsk_leave_room", { room }); export const slskSayRoom = (room, message) => invoke("slsk_say_room", { room, message }); export const slskMessageUser = (username, message) => invoke("slsk_message_user", { username, message }); export const slskRoomList = () => invoke("slsk_room_list"); export const slskBrowseUser = (username) => invoke("slsk_browse_user", { username }); export const slskUserInfo = (username) => invoke("slsk_user_info", { username }); export const slskUserInterests = (username) => invoke("slsk_user_interests", { username }); export const slskSimilarUsers = () => invoke("slsk_similar_users"); export const slskLinkStatus = () => invoke("slsk_link_status"); export const slskLinkSelf = () => invoke("slsk_link_self"); export const slskLinkUnlink = () => invoke("slsk_link_unlink"); export const slskLinkCheck = (username) => invoke("slsk_link_check", { username }); export const slskLinkForget = (username) => invoke("slsk_link_forget", { username }); export const buddyList = () => invoke("buddy_list"); export const buddyAdd = (username) => invoke("buddy_add", { username }); export const buddyRemove = (username) => invoke("buddy_remove", { username }); // `maxEdge` asks the backend for a Lanczos3 thumbnail bounded to that many // pixels on the long side. Omit it only where true pixels matter (the // fullscreen viewer, the import picker's comparison and loupe). export const albumArt = (path, maxEdge = null) => invoke("album_art", { path, maxEdge }); /** Warm the disk thumbnail cache for the whole library at the given edges — * [{warm, decoded}] per edge. Slow by design (one decode at a time); callers * fire-and-forget it. */ export const libraryArtPrewarm = (edges) => invoke("library_art_prewarm", { edges }); /** The cover embedded in a streamed track's own cache file, extracted to a * local image path — null while the transfer is too young to read, or when * the peer's file simply carries no picture. */ export const streamEmbeddedArt = (username, path) => invoke("stream_embedded_art", { username, path }); export const refreshAlbumArt = (albumId) => invoke("refresh_album_art", { albumId }); export const refreshAllAlbumArt = () => invoke("refresh_all_album_art"); export const getSettings = () => invoke("get_settings"); export const setSettings = (patch) => invoke("set_settings", patch); export const windowGlass = (on) => invoke("window_glass", { on }); export const forgetCredentials = () => invoke("forget_credentials"); export const pickFolder = () => invoke("pick_folder"); // ---- playlists (see src-tauri/src/playlists.rs) --------------------------- // Rows are OCCURRENCES with their own ids: the same recording may appear // twice, and every mutation addresses item ids, never positions. export const listPlaylists = () => invoke("list_playlists"); export const createPlaylist = (name, kind = null) => invoke("create_playlist", { name, kind }); export const renamePlaylist = (id, name) => invoke("rename_playlist", { id, name }); export const setPlaylistDescription = (id, description) => invoke("set_playlist_description", { id, description: description ?? null }); export const setPlaylistPinned = (id, pinned) => invoke("set_playlist_pinned", { id, pinned }); export const duplicatePlaylist = (id, name) => invoke("duplicate_playlist", { id, name }); export const deletePlaylist = (id) => invoke("delete_playlist", { id }); export const playlistItems = (id) => invoke("playlist_items", { id }); export const playlistAddItems = (playlistId, items, at = null) => invoke("playlist_add_items", { playlistId, items, at }); export const playlistRemoveItems = (playlistId, itemIds) => invoke("playlist_remove_items", { playlistId, itemIds }); export const playlistSetOrder = (playlistId, orderedIds) => invoke("playlist_set_order", { playlistId, orderedIds }); // Repair one row: fields null = leave as stored; path non-null relinks the // row to a playable file. export const playlistRelinkItem = (playlistId, itemId, fields = {}) => invoke("playlist_relink_item", { playlistId, itemId, title: fields.title ?? null, artist: fields.artist ?? null, album: fields.album ?? null, durationSecs: fields.durationSecs ?? null, path: fields.path ?? null, artPath: fields.artPath ?? null, trackId: fields.trackId ?? null, artUrl: fields.artUrl ?? null, }); // Cover as a data URL: custom master, else composed mosaic, else null. export const playlistCover = (id, maxEdge = null) => invoke("playlist_cover", { id, maxEdge }); export const playlistCoverPick = (id) => invoke("playlist_cover_pick", { id }); export const playlistCoverClear = (id) => invoke("playlist_cover_clear", { id }); // Import: probe opens a file dialog and stages a reviewed draft; text stages // a pasted tracklist; commit writes the accepted draft in one transaction. export const playlistImportProbe = () => invoke("playlist_import_probe"); export const playlistImportFolder = () => invoke("playlist_import_folder"); export const playlistImportText = (text) => invoke("playlist_import_text", { text }); export const playlistImportYoutube = (url) => invoke("playlist_import_youtube", { url }); export const playlistImportYoutubePlaylist = (url) => invoke("playlist_import_youtube_playlist", { url }); // One door for every service link: YouTube, Spotify, Apple Music, TIDAL, Deezer. export const playlistImportLink = (url) => invoke("playlist_import_link", { url }); // A pasted block of TRACK links (any supported service), looked up one page // each — the way past Spotify's 100-track public ceiling. export const playlistImportTrackLinks = (text, name = null) => invoke("playlist_import_track_links", { text, name }); // The set LISTENER (src-tauri/src/setlist_scan.rs + ytdlp.rs): recognize a // mix's tracks from its audio. Progress rides "playlist:scan-progress". export const playlistScanYoutube = (url) => invoke("playlist_scan_youtube", { url }); // The local door is split pick/scan so the modal can collect an optional // pasted tracklist between the two. export const playlistPickSetFile = () => invoke("playlist_pick_set_file"); export const playlistScanFile = (path, tracklist = null) => invoke("playlist_scan_file", { path, tracklist }); export const playlistScanCancel = () => invoke("playlist_scan_cancel"); export const ytdlpStatus = () => invoke("ytdlp_status"); export const ytdlpDownload = () => invoke("ytdlp_download"); export const denoDownload = () => invoke("deno_download"); export const playlistImportCommit = (name, description, kind, entries, coverUrl = null) => invoke("playlist_import_commit", { name, description: description ?? null, kind, entries, coverUrl, }); // Export via a save dialog; format ∈ m3u8 | xspf | csv | json. Resolves to // the written path, or null when the dialog was cancelled. export const playlistExport = (id, format) => invoke("playlist_export", { id, format }); // ---- lyric word-timing preparation (src-tauri/src/lyric_prep.rs) ---------- export const lyricModelStatus = () => invoke("lyric_model_status"); export const lyricModelDownload = () => invoke("lyric_model_download"); // Vocal separation model (src-tauri/src/vocal_sep.rs) — the ctc-v5 stem tier. export const vocalSepStatus = () => invoke("vocal_sep_status"); export const vocalSepDownload = () => invoke("vocal_sep_download"); export const lyricPrepRequest = (path, durationSecs, lines, stream = null) => invoke("lyric_prep_request", { path: path ?? null, stream, durationSecs: durationSecs ?? null, lines, }); // Local-library candidates for the unresolved-row repair panel. export const libraryTrackSearch = (query) => invoke("library_track_search", { query }); // ---- lyrics (see src-tauri/src/lyrics.rs) ---------------------------------- export const lyricsGet = (args) => invoke("lyrics_get", { title: args.title, artist: args.artist, album: args.album ?? null, durationSecs: args.durationSecs ?? null, path: args.path ?? null, refresh: args.refresh ?? null, }); export const lyricsSearch = (title, artist) => invoke("lyrics_search", { title, artist }); export const lyricsSelect = (title, artist, durationSecs, candidateId) => invoke("lyrics_select", { title, artist, durationSecs: durationSecs ?? null, candidateId }); export const lyricsSetOffset = (title, artist, durationSecs, offsetMs) => invoke("lyrics_set_offset", { title, artist, durationSecs: durationSecs ?? null, offsetMs }); export const lyricsSaveText = (title, artist, durationSecs, text) => invoke("lyrics_save_text", { title, artist, durationSecs: durationSecs ?? null, text }); export const lyricsClear = (title, artist, durationSecs) => invoke("lyrics_clear", { title, artist, durationSecs: durationSecs ?? null }); export const lyricsSetAutoSync = (title, artist, durationSecs, enabled) => invoke("lyrics_set_auto_sync", { title, artist, durationSecs: durationSecs ?? null, enabled }); // Offline vocal-reduction render (band-limited center cancel — honest DSP, // not a learned separator). Resolves to { path, cached } of a playable WAV. export const lyricsPrepareInstrumental = (path) => invoke("lyrics_prepare_instrumental", { path }); // Vocal-band activity envelope for the sync editor ({ frameMs, frames }). export const lyricsVocalActivity = (path) => invoke("lyrics_vocal_activity", { path }); // Persist hand-synced lines as the track's own document (wins the waterfall). export const lyricsSaveLines = (title, artist, durationSecs, lines) => invoke("lyrics_save_lines", { title, artist, durationSecs: durationSecs ?? null, lines }); export const catalogSearch = (query) => invoke("catalog_search", { query }); // Track rows only — one Deezer call, and the query may use Deezer's field // syntax (artist:"…" track:"…"). The identity lookups use this; the search // PAGE still uses catalogSearch, which also wants artists and albums. export const catalogSearchTracks = (query, limit = 10) => invoke("catalog_search_tracks", { query, limit }); // The artist page's whole shelf — Deezer plus the MusicBrainz/Apple // supplements — flattened to tracks, for lookups the search index failed. export const catalogArtistTrackCandidates = (artist, title) => invoke("catalog_artist_track_candidates", { artist, title }); export const catalogTrackByIsrc = (isrc) => invoke("catalog_track_by_isrc", { isrc }); export const rememberFusedArtistPortrait = (artistId, portrait) => invoke("remember_fused_artist_portrait", { artistId, portrait }); export const catalogArtist = (artistId) => checkedArtistPayload("catalog_artist", artistId); export const catalogArtistArt = (artistId) => checkedArtistPayload("catalog_artist_art", artistId); export const catalogArtistLogos = (artistId) => invoke("catalog_artist_logos", { artistId }); // Deferred MusicBrainz pass (socials + MB-only releases). Rate-limited at // 1 req/s upstream, so it is fetched after the page renders, never before. export const catalogArtistSocials = (artistId) => invoke("catalog_artist_socials", { artistId }); export const catalogArtistBio = (artistId, artist) => invoke("catalog_artist_bio", { artistId: artistId ?? null, artist }); export const catalogArtistIdentity = (artistId, artist) => checkedArtistIdentity(artistId, artist); export const catalogCachedCoverLogo = (artistId) => invoke("catalog_cached_cover_logo", { artistId }); export const catalogCoverLogo = (artistId, artist, covers) => invoke("catalog_cover_logo", { artistId, artist, covers }); export const catalogArtistHeroSeed = (artistId) => invoke("catalog_artist_hero_seed", { artistId }); export const catalogArtistOverview = (artistId) => checkedArtistPayload("catalog_artist_overview", artistId); // Popular's fast lane: the Last.fm-ordered rows with listener counts, waiting // on nothing but the linked MBID and one Last.fm call. Patched over the // overview's budgeted (cold: unranked) list; the full page still lands last. export const catalogArtistPopular = (artistId) => invoke("catalog_artist_popular", { artistId }); export const pickArtistHero = (artistId) => invoke("pick_artist_hero", { artistId }); export const clearArtistHero = (artistId) => invoke("clear_artist_hero", { artistId }); export const catalogRelated = (artistId) => invoke("catalog_related", { artistId }); // Deferred Last.fm pass for search results: `tracks` is [{ artistId, title }] // and the answer comes back in the same order. Kept OUT of catalog_search, // which the typeahead calls per keystroke and which must never wait on // MusicBrainz's 1 req/s gate. export const catalogTrackListeners = (tracks) => invoke("catalog_track_listeners", { tracks }); // Genres + Last.fm totals for the search hero's artist. Deferred, like the rest. export const catalogArtistProfile = (artistId) => invoke("catalog_artist_profile", { artistId }); export const catalogAlbum = (albumId) => checkedAlbumPayload(albumId); export const catalogAlbumInfo = (artistId, artist, title) => invoke("catalog_album_info", { artistId: artistId ?? null, artist, title }); export const catalogArt = (url) => invoke("catalog_art", { url }); export const catalogCover = (artistId, artist, title, cover) => invoke("catalog_cover", { artistId: artistId ?? null, artist, title, cover }); export const invalidateCatalogCover = (artistId, artist, title, cover) => invoke("invalidate_catalog_cover", { artistId: artistId ?? null, artist, title, cover }); export const catalogHeroPreview = (url) => invoke("catalog_hero_preview", { url }); export const catalogHeroGpuSource = (url, plan = null) => invoke("catalog_hero_gpu_source", { url, plan }); export const catalogCachedHeroArt = (url) => invoke("catalog_cached_hero_art", { url }); export const catalogHeroArt = (url, plan = null) => invoke("catalog_hero_art", { url, plan }); export const preloadStream = (username, path, size) => invoke("preload_stream", { username, path, size }); export const preloadCachedStream = (username, path, size, duration) => invoke("preload_cached_stream", { username, path, size, duration }); export const streamWaveform = (username, path, size, buckets) => invoke("stream_waveform", { username, path, size, buckets }); export const streamCacheReady = (username, path, size) => invoke("stream_cache_ready", { username, path, size }); // Preview tracks (scrollable-track-previews): a fully separate audio path. // previewResolve returns { ok, lease | failure, retryable } — the signed URL // never crosses this boundary, only an opaque lease id does. export const previewResolve = (trackId) => invoke("preview_resolve", { trackId }); // Fire-and-forget session prewarm: pulls every listed clip onto disk ahead // of the user (paced, capped, cancelled by dispose). The invoke resolves // when the walk finishes — callers void it. export const previewPrewarm = (trackIds) => invoke("preview_prewarm", { trackIds }); export const previewLoad = (leaseId, startPaused, crossfadeMs = 0, startAtSecs = null) => invoke("preview_load", { leaseId, startPaused, crossfadeMs, startAtSecs }); export const playerFadePauseAcked = (fadeMs) => invoke("player_fade_pause_acked", { fadeMs }); export const playerFadeResume = (fadeMs) => invoke("player_fade_resume", { fadeMs }); export const debugStats = () => invoke("debug_stats"); // ---- the import tool (see src-tauri/src/import.rs) ------------------------ // Scan is read-only; apply COPIES into the managed root and registers — the // source folder is never written to. export const importScan = (root) => invoke("import_scan", { root }); export const importSessionGet = () => invoke("import_session_get"); // The source folder relative to the library folder, and back again. Saving // both is what lets a review started on one machine resume on another after // the drive lands at a different letter. export const importPortableRoot = (root) => invoke("import_portable_root", { root }); export const importResolveRoot = (saved, rel) => invoke("import_resolve_root", { saved, rel }); export const importSessionSet = (state) => invoke("import_session_set", { state }); export const importSessionClear = () => invoke("import_session_clear"); export const importGetDest = () => invoke("import_get_dest"); export const importSetDest = (dir, layout = null) => invoke("import_set_dest", { dir, layout }); export const importApply = (sourceRoot, albums) => invoke("import_apply", { sourceRoot, albums }); // On-demand per-record identification (Deezer try-list → MB fallback) and // the artwork panel's cover report. Both read-only. // `title` is the record's own claim (tags); `folder` is the second witness, // asked after it — a record whose tags are wrong still has its folder name. // `shape` is everything ELSE the scan knows — track count, track titles, year, // and where the files are so Rust can hash the record's own cover. That is what // the discography pass compares when no phrase can find the record; the cover // itself never crosses IPC (a master can be 8 MB). export const importIdentify = (artist, title, folder = null, shape = {}) => invoke("import_identify", { artist, title, folder, tracks: shape.tracks ?? null, trackTitles: shape.trackTitles ?? null, year: shape.year ?? null, sourceRoot: shape.sourceRoot ?? null, rel: shape.rel ?? null, // Bounds the cover hash to the record's own audio — a folded release // gathers from these, an ordinary one from its folder alone. parts: shape.parts ?? null, }); export const importCoverReport = (artist, title, referenceUrl) => invoke("import_cover_report", { artist, title, referenceUrl }); // The record's own artwork, measured (embedded first, else folder images). export const importLocalArt = (sourceRoot, rel, parts) => invoke("import_local_art", { sourceRoot, rel, parts }); // Prune rows whose files are gone, then index anything new — the real rescan. export const rescanLibrary = () => invoke("rescan_library"); // Download a found album copy into (Cache)\_audition\\ for the version // comparison. Called track 1 first, then the rest — `startIndex` keeps the // second batch's indices continuing, since index 0 means "track 1". export const importAudition = (username, rel, files, startIndex = 0) => invoke("import_audition", { username, rel, files, startIndex }); export const onImportAudition = (cb) => listen("import:audition", (e) => cb(e.payload)); // Which of a found copy's files the pen does not (fully) hold. Transfers die // with the app and the arrived-count dies with them; the files on disk are // what is left to ask. export const importAuditionMissing = (rel, files) => invoke("import_audition_missing", { rel, files }); // Empty a record's pen when its peer is abandoned; returns the bytes freed. // The pen is keyed by RECORD, so without this the dropped peer's files sit // beside the replacement's until the session is cleared. export const importAuditionClear = (rel) => invoke("import_audition_clear", { rel }); // Measured facts about one audio file (the version card's quality chips). export const importProbeFile = (path) => invoke("import_probe_file", { path }); export const onImportScan = (cb) => listen("import:scan", (e) => cb(e.payload)); export const onImportScanFolder = (cb) => listen("import:scan-folder", (e) => cb(e.payload)); export const onImportApply = (cb) => listen("import:apply", (e) => cb(e.payload)); export const previewControl = (action, value = null) => invoke("preview_control", { action, value }); export const previewDispose = () => invoke("preview_dispose"); // Main-player silence acknowledgement (audio-focus contract). Throws on // timeout — callers keep preview parked; they never assume silence. export const playerPauseAcked = () => invoke("player_pause_acked"); // Window chrome (the app draws its own titlebar; decorations are off). // Guarded like onEvent below: getCurrentWindow() THROWS outside Tauri, and // it runs at mount via initWindowActivity — un-guarded it made every plain // `vite` browser load a blank page, which also blocked browser-side preview // fixtures. In-Tauri behavior is unchanged. import { getCurrentWindow } from "@tauri-apps/api/window"; const inTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; const noWin = { minimize: () => {}, toggleMaximize: () => {}, close: () => {}, isMinimized: async () => false, isFocused: async () => true, isMaximized: async () => false, onResized: async () => () => {}, onFocusChanged: async () => () => {}, }; export const win = !inTauri ? noWin : { minimize: () => getCurrentWindow().minimize(), toggleMaximize: () => getCurrentWindow().toggleMaximize(), close: () => getCurrentWindow().close(), isMinimized: () => getCurrentWindow().isMinimized(), isFocused: () => getCurrentWindow().isFocused(), isMaximized: () => getCurrentWindow().isMaximized(), // returns Promise onResized: (handler) => getCurrentWindow().onResized(handler), onFocusChanged: (handler) => getCurrentWindow().onFocusChanged(handler), }; // Subscribe to a backend event; returns an unlisten function. // No-ops outside Tauri (e.g. plain-browser preview). export function onEvent(name, handler) { const un = listen(name, (e) => handler(e.payload)).catch(() => () => {}); return () => un.then((f) => f()); } // ---- the Community area: Freenet layer, persona, profile, local contacts ---- export const freenetStatus = () => invoke("freenet_status"); export const freenetSetEnabled = (enabled) => invoke("freenet_set_enabled", { enabled }); export const freenetDetect = () => invoke("freenet_detect"); export const freenetSetLoadRemoteImages = (on) => invoke("freenet_set_load_remote_images", { on }); export const freenetNodeInfo = () => invoke("freenet_node_info"); export const freenetNodeStart = () => invoke("freenet_node_start"); export const freenetNodeStop = () => invoke("freenet_node_stop"); export const personaStatus = () => invoke("persona_status"); export const personaCreate = () => invoke("persona_create"); export const personaExport = (passphrase, path) => invoke("persona_export", { passphrase, path }); export const personaImport = (passphrase, path, force = false) => invoke("persona_import", { passphrase, path, force }); export const personaRotate = () => invoke("persona_rotate"); export const personaRemove = () => invoke("persona_remove"); export const profileGet = (persona = null, force = false) => invoke("profile_get", { persona, force }); export const profilePublish = (displayName, bio) => invoke("profile_publish", { displayName, bio }); export const profileSetAvatar = (imagePath) => invoke("profile_set_avatar", { imagePath }); export const profileClearAvatar = () => invoke("profile_clear_avatar"); export const contactsMetaList = () => invoke("contacts_meta_list"); export const contactMetaSet = (meta) => invoke("contact_meta_set", { meta }); /** kind: "image" | "recovery-open" | "recovery-save" -> a path, or null. */ export const communityPickFile = (kind) => invoke("community_pick_file", { kind });