mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Chore: Port over SeriesDetail (WIP Panels)
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
import { open as openUrl } from '@tauri-apps/plugin-shell'
|
||||
import { platformService } from '$lib/platform-service'
|
||||
import { autoBackupAppData } from '$lib/core/backup'
|
||||
import { requestManager } from '$lib/request-manager'
|
||||
import type { ReleaseInfo } from '$lib/platform-adapters/types'
|
||||
|
||||
interface ReleaseInfo { tag_name: string; name: string; body: string; published_at: string; html_url: string }
|
||||
type UpdatePhase = 'idle' | 'downloading' | 'launching' | 'ready' | 'error'
|
||||
const IS_WINDOWS = navigator.userAgent.includes('Windows')
|
||||
|
||||
const supportsUpdates = platformService.isSupported('app-updates')
|
||||
const IS_WINDOWS = navigator.userAgent.includes('Windows')
|
||||
|
||||
interface AboutServer { name: string; version: string; buildType: string; buildTime: number; github: string; discord: string }
|
||||
interface AboutWebUI { channel: string; tag: string; updateTimestamp: number }
|
||||
@@ -29,32 +28,33 @@
|
||||
let webuiInfo = $state<AboutWebUI | null>(null)
|
||||
|
||||
$effect(() => {
|
||||
getVersion().then(v => appVersion = v).catch(() => appVersion = 'unknown')
|
||||
platformService.getVersion().then(v => appVersion = v).catch(() => appVersion = 'unknown')
|
||||
if (!releasesLoaded) { releasesLoaded = true; loadReleases() }
|
||||
loadServerInfo()
|
||||
})
|
||||
|
||||
$effect(() => { loadServerInfo() })
|
||||
|
||||
$effect(() => {
|
||||
if (!supportsUpdates) return
|
||||
let unlisten: (() => void) | undefined
|
||||
listen<{ downloaded: number; total: number | null }>('update-progress', e => {
|
||||
dlBytes = e.payload.downloaded; dlTotal = e.payload.total ?? null
|
||||
platformService.onUpdateProgress(p => {
|
||||
dlBytes = p.downloaded; dlTotal = p.total ?? null
|
||||
}).then(fn => { unlisten = fn })
|
||||
return () => unlisten?.()
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!supportsUpdates) return
|
||||
let unlisten: (() => void) | undefined
|
||||
listen('update-launching', () => { updatePhase = 'launching' }).then(fn => { unlisten = fn })
|
||||
platformService.onUpdateLaunching(() => { updatePhase = 'launching' }).then(fn => { unlisten = fn })
|
||||
return () => unlisten?.()
|
||||
})
|
||||
|
||||
async function loadReleases() {
|
||||
if (!supportsUpdates) return
|
||||
releasesLoading = true; releasesError = null
|
||||
try {
|
||||
const timeout = new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Request timed out after 10s')), 10_000))
|
||||
const all = await Promise.race([invoke<ReleaseInfo[]>('list_releases'), timeout])
|
||||
releases = all.filter(r => typeof r.tag_name === 'string' && r.tag_name.trim())
|
||||
releases = await Promise.race([platformService.listReleases(), timeout])
|
||||
} catch (e: any) {
|
||||
releasesError = e instanceof Error ? e.message : String(e)
|
||||
} finally { releasesLoading = false }
|
||||
@@ -81,7 +81,7 @@
|
||||
}
|
||||
|
||||
const onLatestVersion = $derived((() => {
|
||||
if (releasesLoading || releases.length === 0 || !appVersion || appVersion === '…') return false
|
||||
if (!supportsUpdates || releasesLoading || releases.length === 0 || !appVersion || appVersion === '…') return false
|
||||
const sorted = releases.slice().sort((a, b) => compareSemver(a.tag_name, b.tag_name))
|
||||
return compareSemver(appVersion, sorted[0].tag_name) >= 0
|
||||
})())
|
||||
@@ -115,11 +115,11 @@
|
||||
try {
|
||||
if (IS_WINDOWS) {
|
||||
await autoBackupAppData()
|
||||
try { await invoke('kill_server') } catch {}
|
||||
await invoke('download_and_install_update', { tag: release.tag_name })
|
||||
try { await platformService.stopServer() } catch {}
|
||||
await platformService.installAppUpdate(release.tag_name)
|
||||
updatePhase = 'ready'
|
||||
} else {
|
||||
await openUrl(release.html_url)
|
||||
await platformService.openExternal(release.html_url)
|
||||
updatePhase = 'idle'; targetTag = null
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -128,7 +128,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function restartNow() { await invoke('restart_app') }
|
||||
async function restartNow() { await platformService.restartApp() }
|
||||
function cancelUpdate() { updatePhase = 'idle'; updateError = null; targetTag = null; dlBytes = 0; dlTotal = null }
|
||||
</script>
|
||||
|
||||
@@ -149,9 +149,11 @@
|
||||
<div class="s-section-body">
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><span class="s-label">Installed</span><span class="s-desc">v{appVersion}</span></div>
|
||||
<button class="s-btn" onclick={() => { releasesError = null; loadReleases() }} disabled={releasesLoading}>
|
||||
{releasesLoading ? 'Loading…' : 'Refresh'}
|
||||
</button>
|
||||
{#if supportsUpdates}
|
||||
<button class="s-btn" onclick={() => { releasesError = null; loadReleases() }} disabled={releasesLoading}>
|
||||
{releasesLoading ? 'Loading…' : 'Refresh'}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if onLatestVersion}
|
||||
<div class="s-row">
|
||||
@@ -225,58 +227,60 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="s-section">
|
||||
<p class="s-section-title">Releases</p>
|
||||
<div class="s-section-body">
|
||||
{#if releasesError}
|
||||
<p class="s-empty" style="color:var(--color-error)">{releasesError}</p>
|
||||
{:else if releasesLoading}
|
||||
<p class="s-empty">Fetching releases…</p>
|
||||
{:else if releases.length === 0}
|
||||
<p class="s-empty">No releases found.</p>
|
||||
{:else}
|
||||
<div class="s-release-scroll">
|
||||
{#each releases as release}
|
||||
{@const isCurrent = isCurrentVersion(release.tag_name)}
|
||||
{@const isExpanded = expandedTag === release.tag_name}
|
||||
{@const isTarget = targetTag === release.tag_name}
|
||||
{@const isInstalling = isTarget && updatePhase === 'downloading'}
|
||||
<div class="s-release-row" class:current={isCurrent}>
|
||||
<div class="s-release-header">
|
||||
<div class="s-release-meta">
|
||||
<span class="s-release-tag">{release.tag_name}</span>
|
||||
{#if isCurrent}<span class="s-release-badge">installed</span>{/if}
|
||||
{#if release.published_at}<span class="s-release-date">{fmtDate(release.published_at)}</span>{/if}
|
||||
</div>
|
||||
<div class="s-btn-row">
|
||||
{#if release.body.trim()}
|
||||
<button class="s-btn" onclick={() => expandedTag = isExpanded ? null : release.tag_name}>
|
||||
{isExpanded ? 'Hide' : 'Changelog'}
|
||||
</button>
|
||||
{/if}
|
||||
{#if !isCurrent}
|
||||
{#if IS_WINDOWS}
|
||||
<button class="s-btn" class:s-btn-accent={!isInstalling}
|
||||
disabled={updatePhase === 'downloading'} onclick={() => installUpdate(release)}>
|
||||
{isInstalling ? 'Downloading…' : 'Install'}
|
||||
{#if supportsUpdates}
|
||||
<div class="s-section">
|
||||
<p class="s-section-title">Releases</p>
|
||||
<div class="s-section-body">
|
||||
{#if releasesError}
|
||||
<p class="s-empty" style="color:var(--color-error)">{releasesError}</p>
|
||||
{:else if releasesLoading}
|
||||
<p class="s-empty">Fetching releases…</p>
|
||||
{:else if releases.length === 0}
|
||||
<p class="s-empty">No releases found.</p>
|
||||
{:else}
|
||||
<div class="s-release-scroll">
|
||||
{#each releases as release}
|
||||
{@const isCurrent = isCurrentVersion(release.tag_name)}
|
||||
{@const isExpanded = expandedTag === release.tag_name}
|
||||
{@const isTarget = targetTag === release.tag_name}
|
||||
{@const isInstalling = isTarget && updatePhase === 'downloading'}
|
||||
<div class="s-release-row" class:current={isCurrent}>
|
||||
<div class="s-release-header">
|
||||
<div class="s-release-meta">
|
||||
<span class="s-release-tag">{release.tag_name}</span>
|
||||
{#if isCurrent}<span class="s-release-badge">installed</span>{/if}
|
||||
{#if release.published_at}<span class="s-release-date">{fmtDate(release.published_at)}</span>{/if}
|
||||
</div>
|
||||
<div class="s-btn-row">
|
||||
{#if release.body.trim()}
|
||||
<button class="s-btn" onclick={() => expandedTag = isExpanded ? null : release.tag_name}>
|
||||
{isExpanded ? 'Hide' : 'Changelog'}
|
||||
</button>
|
||||
{:else}
|
||||
<button class="s-btn" onclick={() => installUpdate(release)}>Open on GitHub</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !isCurrent}
|
||||
{#if IS_WINDOWS}
|
||||
<button class="s-btn" class:s-btn-accent={!isInstalling}
|
||||
disabled={updatePhase === 'downloading'} onclick={() => installUpdate(release)}>
|
||||
{isInstalling ? 'Downloading…' : 'Install'}
|
||||
</button>
|
||||
{:else}
|
||||
<button class="s-btn" onclick={() => installUpdate(release)}>Open on GitHub</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if isExpanded && release.body.trim()}
|
||||
<div class="s-release-body">
|
||||
<pre class="s-release-body pre">{release.body.trim()}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isExpanded && release.body.trim()}
|
||||
<div class="s-release-body">
|
||||
<pre class="s-release-body pre">{release.body.trim()}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="s-section">
|
||||
<p class="s-section-title">Links</p>
|
||||
|
||||
@@ -114,14 +114,16 @@
|
||||
const reordered = [...sortable]
|
||||
const [moved] = reordered.splice(sFromIdx, 1)
|
||||
reordered.splice(sToIdx, 0, moved)
|
||||
categories = [...zeroCat, ...reordered.map((c, i) => ({ ...c, order: i + 1 }))]
|
||||
getAdapter().updateCategoryOrder({ id: fromNumId, position: sToIdx + 1 })
|
||||
const optimistic = [...zeroCat, ...reordered.map((c, i) => ({ ...c, order: i + 1 }))]
|
||||
categories = optimistic
|
||||
const serverPosition = sToIdx + 1
|
||||
getAdapter().updateCategoryOrder({ id: fromNumId, position: serverPosition })
|
||||
.then((updated: Category[]) => {
|
||||
categories = [
|
||||
...zeroCat,
|
||||
...updated.sort((a: Category, b: Category) => a.order - b.order).map((fresh: Category) => {
|
||||
const existing = categories.find(c => c.id === fresh.id)
|
||||
return existing ? { ...existing, ...fresh } : fresh
|
||||
const local = optimistic.find(c => c.id === fresh.id)
|
||||
return local ? { ...fresh, mangas: local.mangas } : fresh
|
||||
}),
|
||||
]
|
||||
})
|
||||
@@ -203,7 +205,7 @@
|
||||
<DotsSixVertical size={14} weight="bold" />
|
||||
</span>
|
||||
<span class="s-folder-name">{cat?.name ?? 'Completed'}</span>
|
||||
<span class="s-folder-count">{cat?.mangas?.nodes.length ?? 0} manga</span>
|
||||
<span class="s-folder-count">{cat?.mangas?.nodes?.length ?? 0} manga</span>
|
||||
<span class="s-folder-badge">built-in</span>
|
||||
<div class="s-folder-actions">
|
||||
<button class="s-btn-icon" class:muted={hidden} onclick={() => toggleHidden(id)} title={hidden ? 'Show tab in library' : 'Hide tab from library'}>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script lang="ts">
|
||||
export { eventToKeybind, matchesKeybind } from './keybindEngine'
|
||||
export { DEFAULT_KEYBINDS, KEYBIND_LABELS } from './defaultBinds'
|
||||
export type { Keybinds } from './defaultBinds'
|
||||
import { settingsState, updateSettings } from '$lib/state/settings.svelte'
|
||||
import { eventToKeybind } from '$lib/core/keybinds/keybindEngine'
|
||||
import { DEFAULT_KEYBINDS, KEYBIND_LABELS } from '$lib/core/keybinds/defaultBinds'
|
||||
import type { Keybinds } from '$lib/core/keybinds/defaultBinds'
|
||||
|
||||
function resetKeybinds() {
|
||||
updateSettings({ keybinds: { ...DEFAULT_KEYBINDS } })
|
||||
}
|
||||
|
||||
let listeningKey: keyof Keybinds | null = $state(null)
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Trash, ClockCounterClockwise } from 'phosphor-svelte'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { untrack } from 'svelte'
|
||||
import { appState } from '$lib/state/app.svelte'
|
||||
import { platformService } from '$lib/platform-service'
|
||||
import { toast } from '$lib/state/notifications.svelte'
|
||||
import { settingsState, updateSettings } from '$lib/state/settings.svelte'
|
||||
import { exportAppData, importAppData } from '$lib/core/backup'
|
||||
@@ -14,6 +13,8 @@
|
||||
import { clearPageCache } from '$lib/request-manager'
|
||||
import { cache as queryCache } from '$lib/core/cache/queryCache'
|
||||
|
||||
const supportsFilesystem = platformService.isSupported('filesystem')
|
||||
|
||||
type ResetState = 'idle' | 'busy' | 'done' | 'error'
|
||||
interface ResetItem { key: string; label: string; desc: string; state: ResetState; error: string | null; confirm: boolean }
|
||||
|
||||
@@ -76,8 +77,8 @@
|
||||
clearPageCache()
|
||||
queryCache.clearAll()
|
||||
await Promise.all([
|
||||
invoke('clear_moku_cache'),
|
||||
invoke('clear_suwayomi_cache'),
|
||||
platformService.clearMokuCache(),
|
||||
platformService.clearSuwayomiCache(),
|
||||
gql(`mutation { clearCachedImages(input: { cachedPages: true, cachedThumbnails: true, downloadedThumbnails: false }) { cachedPages cachedThumbnails } }`),
|
||||
])
|
||||
}
|
||||
@@ -98,14 +99,14 @@
|
||||
await persistSettings({ settings: DEFAULT_SETTINGS, storeVersion: 1 })
|
||||
patchReset(key, { state: 'done' })
|
||||
await showExitCountdown()
|
||||
invoke('exit_app')
|
||||
platformService.exitApp()
|
||||
return
|
||||
case 'suwayomi-data':
|
||||
localStorage.clear()
|
||||
await invoke('reset_suwayomi_data')
|
||||
await platformService.resetSuwayomiData()
|
||||
patchReset(key, { state: 'done' })
|
||||
await showExitCountdown()
|
||||
invoke('exit_app')
|
||||
platformService.exitApp()
|
||||
return
|
||||
}
|
||||
patchReset(key, { state: 'done' })
|
||||
@@ -138,8 +139,9 @@
|
||||
|
||||
let defaultDownloadsPath = $state('')
|
||||
$effect(() => {
|
||||
if (!supportsFilesystem) return
|
||||
if (!isExternalServer) {
|
||||
invoke<string>('get_default_downloads_path').then(p => { defaultDownloadsPath = p })
|
||||
platformService.getDefaultDownloadsPath().then(p => { defaultDownloadsPath = p })
|
||||
} else {
|
||||
defaultDownloadsPath = ''
|
||||
}
|
||||
@@ -163,6 +165,7 @@
|
||||
let resetSectionOpen = $state(false)
|
||||
|
||||
async function fetchStorage() {
|
||||
if (!supportsFilesystem) return
|
||||
storageLoading = true; storageError = null
|
||||
try {
|
||||
const pathData = await gql<{ downloadsPath: string | null; localSourcePath: string | null }>(
|
||||
@@ -183,7 +186,7 @@
|
||||
}
|
||||
if (dirsToScan.length === 0) { multiStorageInfos = []; storageInfo = null; return }
|
||||
const results = await Promise.allSettled(
|
||||
dirsToScan.map(d => invoke<StorageInfo>('get_storage_info', { downloadsPath: d.path }).then(info => ({ ...info, label: d.label })))
|
||||
dirsToScan.map(d => platformService.getStorageInfo(d.path).then(info => ({ ...info, label: d.label })))
|
||||
)
|
||||
multiStorageInfos = results
|
||||
.filter((r): r is PromiseFulfilledResult<StorageInfo & { label: string }> => r.status === 'fulfilled')
|
||||
@@ -195,17 +198,16 @@
|
||||
}
|
||||
|
||||
async function validatePath(path: string): Promise<string | null> {
|
||||
if (!path.trim()) return null
|
||||
if (isExternalServer) return null
|
||||
if (!path.trim() || isExternalServer || !supportsFilesystem) return null
|
||||
try {
|
||||
const exists = await invoke<boolean>('check_path_exists', { path: path.trim() })
|
||||
const exists = await platformService.checkPathExists(path.trim())
|
||||
return exists ? null : 'Directory does not exist'
|
||||
} catch { return 'Could not check path' }
|
||||
}
|
||||
|
||||
async function createDirectory(path: string): Promise<void> {
|
||||
if (isExternalServer) throw new Error('Cannot create directories on an external server')
|
||||
await invoke('create_directory', { path })
|
||||
await platformService.createDirectory(path)
|
||||
}
|
||||
|
||||
async function savePaths() {
|
||||
@@ -219,11 +221,11 @@
|
||||
await gql(`mutation($path: String!) { setDownloadsPath(input: { location: $path }) { location } }`, { path: dl })
|
||||
if (loc) await gql(`mutation($path: String!) { setLocalSourcePath(input: { location: $path }) { location } }`, { path: loc })
|
||||
updateSettings({ serverDownloadsPath: dl, serverLocalSourcePath: loc })
|
||||
if (!isExternalServer) {
|
||||
if (supportsFilesystem && !isExternalServer) {
|
||||
const oldDl = confirmedDownloadsPath || defaultDownloadsPath
|
||||
const newDl = dl || defaultDownloadsPath
|
||||
if (newDl && oldDl && newDl !== oldDl) {
|
||||
const hadContent = await invoke<boolean>('check_path_exists', { path: oldDl })
|
||||
const hadContent = await platformService.checkPathExists(oldDl)
|
||||
if (hadContent) { migrateFrom = oldDl; migrateTo = newDl }
|
||||
}
|
||||
}
|
||||
@@ -238,12 +240,9 @@
|
||||
async function startMigration() {
|
||||
if (!migrateFrom || !migrateTo) return
|
||||
migrating = true; migrateError = null; migrateProgress = { done: 0, total: 0, current: '' }
|
||||
const { listen: tauriListen } = await import('@tauri-apps/api/event')
|
||||
migrateUnlisten = await tauriListen<{ done: number; total: number; current: string }>(
|
||||
'migrate_progress', e => { migrateProgress = e.payload }
|
||||
)
|
||||
migrateUnlisten = await platformService.onMigrateProgress(p => { migrateProgress = p })
|
||||
try {
|
||||
await invoke('migrate_downloads', { src: migrateFrom, dst: migrateTo })
|
||||
await platformService.migrateDownloads(migrateFrom, migrateTo)
|
||||
migrateFrom = null; migrateTo = null; migrateProgress = null
|
||||
await fetchStorage()
|
||||
} catch (e: any) {
|
||||
@@ -254,17 +253,17 @@
|
||||
function dismissMigration() { migrateFrom = null; migrateTo = null; migrateError = null; migrateProgress = null }
|
||||
|
||||
async function browseDownloadsFolder() {
|
||||
const picked = await invoke<string | null>('pick_downloads_folder')
|
||||
const picked = await platformService.pickFolder()
|
||||
if (picked) { downloadsPathInput = picked; pathsFieldError = { ...pathsFieldError, dl: undefined }; await savePaths() }
|
||||
}
|
||||
|
||||
async function browseLocalSourceFolder() {
|
||||
const picked = await invoke<string | null>('pick_downloads_folder')
|
||||
const picked = await platformService.pickFolder()
|
||||
if (picked) { localSourcePathInput = picked; pathsFieldError = { ...pathsFieldError, loc: undefined }; await savePaths() }
|
||||
}
|
||||
|
||||
async function browseExtraScanDir() {
|
||||
const picked = await invoke<string | null>('pick_downloads_folder')
|
||||
const picked = await platformService.pickFolder()
|
||||
if (picked) { newScanDir = picked; addExtraScanDir() }
|
||||
}
|
||||
|
||||
@@ -410,6 +409,11 @@
|
||||
restoreLoading = true; restoreError = null; restoreStatus = null; restoreJobId = null
|
||||
stopRestorePoll()
|
||||
try {
|
||||
const form = buildBackupFormData(
|
||||
restoreFile,
|
||||
`mutation RestoreBackup($backup: Upload!) { restoreBackup(input: { backup: $backup }) { id status { mangaProgress state totalManga } } }`,
|
||||
{ backup: null }
|
||||
)
|
||||
const resp = await fetch(`${serverUrl()}/api/graphql`, { method: 'POST', headers: buildAuthHeaders(), body: form })
|
||||
const json = await resp.json()
|
||||
if (json.errors?.length) throw new Error(json.errors[0].message)
|
||||
@@ -445,7 +449,8 @@
|
||||
let appDataBackupDir = $state<string | null>(null)
|
||||
|
||||
$effect(() => {
|
||||
invoke<string>('get_auto_backup_dir').then(d => { appDataBackupDir = d }).catch(() => {})
|
||||
if (!supportsFilesystem) return
|
||||
platformService.getAutoBackupDir().then(d => { appDataBackupDir = d }).catch(() => {})
|
||||
})
|
||||
|
||||
async function handleExportAppData() {
|
||||
@@ -507,6 +512,8 @@
|
||||
<p class="s-empty">Reading filesystem…</p>
|
||||
{:else if storageError}
|
||||
<p class="s-empty" style="color:var(--color-error)">{storageError}</p>
|
||||
{:else if !supportsFilesystem}
|
||||
<p class="s-empty">Disk usage is unavailable in web mode.</p>
|
||||
{:else if isExternalServer}
|
||||
<p class="s-empty">Disk usage is unavailable for external servers — filesystem access requires a local connection.</p>
|
||||
{:else if multiStorageInfos.length > 0}
|
||||
@@ -551,7 +558,7 @@
|
||||
spellcheck="false"
|
||||
onkeydown={(e) => e.key === 'Enter' && savePaths()}
|
||||
oninput={() => { pathsFieldError = { ...pathsFieldError, dl: undefined } }} />
|
||||
{#if !isExternalServer}
|
||||
{#if !isExternalServer && supportsFilesystem}
|
||||
<button class="s-btn" onclick={browseDownloadsFolder}>Browse</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -565,7 +572,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="s-btn-row">
|
||||
{#if pathsFieldError.dl && !isExternalServer}
|
||||
{#if pathsFieldError.dl && !isExternalServer && supportsFilesystem}
|
||||
<button class="s-btn" onclick={async () => {
|
||||
try { await createDirectory(downloadsPathInput.trim()); pathsFieldError = { ...pathsFieldError, dl: undefined } }
|
||||
catch (e: any) { pathsFieldError = { ...pathsFieldError, dl: e?.message ?? 'Failed' } }
|
||||
@@ -624,10 +631,10 @@
|
||||
bind:value={localSourcePathInput} placeholder="Optional" spellcheck="false"
|
||||
onkeydown={(e) => e.key === 'Enter' && savePaths()}
|
||||
oninput={() => { pathsFieldError = { ...pathsFieldError, loc: undefined } }} />
|
||||
{#if !isExternalServer}
|
||||
{#if !isExternalServer && supportsFilesystem}
|
||||
<button class="s-btn" onclick={browseLocalSourceFolder}>Browse</button>
|
||||
{/if}
|
||||
{#if pathsFieldError.loc && !isExternalServer}
|
||||
{#if pathsFieldError.loc && !isExternalServer && supportsFilesystem}
|
||||
<button class="s-btn" onclick={async () => {
|
||||
try { await createDirectory(localSourcePathInput.trim()); pathsFieldError = { ...pathsFieldError, loc: undefined } }
|
||||
catch (e: any) { pathsFieldError = { ...pathsFieldError, loc: e?.message ?? 'Failed' } }
|
||||
@@ -656,7 +663,7 @@
|
||||
<div class="s-btn-row">
|
||||
<input class="s-input mono" bind:value={newScanDir} placeholder="/path/to/dir" spellcheck="false"
|
||||
onkeydown={(e) => e.key === 'Enter' && addExtraScanDir()} />
|
||||
{#if !isExternalServer}
|
||||
{#if !isExternalServer && supportsFilesystem}
|
||||
<button class="s-btn" onclick={browseExtraScanDir}>Browse</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -790,7 +797,7 @@
|
||||
<span class="s-label">Export settings</span>
|
||||
<span class="s-desc">Save all Moku app settings to a .zip via a native save dialog.</span>
|
||||
</div>
|
||||
<button class="s-btn s-btn-accent" onclick={handleExportAppData} disabled={appDataExporting}>
|
||||
<button class="s-btn s-btn-accent" onclick={handleExportAppData} disabled={appDataExporting || !supportsFilesystem}>
|
||||
{appDataExporting ? 'Saving…' : 'Export'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -800,7 +807,7 @@
|
||||
<span class="s-label">Import settings</span>
|
||||
<span class="s-desc">Restore from a previously exported .zip file. Reloads the app immediately.</span>
|
||||
</div>
|
||||
<button class="s-btn" onclick={handleImportAppData} disabled={appDataImporting}>
|
||||
<button class="s-btn" onclick={handleImportAppData} disabled={appDataImporting || !supportsFilesystem}>
|
||||
{appDataImporting ? 'Importing…' : 'Import'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -821,7 +828,7 @@
|
||||
<span class="s-label">Auto-backup location</span>
|
||||
<span class="s-desc">Pre-update snapshots are kept here (last 5).</span>
|
||||
</div>
|
||||
<button class="s-btn" onclick={() => invoke('open_path', { path: appDataBackupDir })}>Open folder</button>
|
||||
<button class="s-btn" onclick={() => platformService.openPath(appDataBackupDir!)}>Open folder</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user