mirror of
https://github.com/markuryy/shark.git
synced 2026-06-18 18:41:03 +00:00
feat(device): add device sync button
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
play: '/icons/speaker.png',
|
||||
search: '/icons/internet.png',
|
||||
computer: '/icons/computer.png',
|
||||
device: '/icons/ipod.svg',
|
||||
};
|
||||
|
||||
let history: string[] = $state([]);
|
||||
@@ -80,7 +81,12 @@
|
||||
<img src={icons.search} alt="Search" />
|
||||
<span>Search</span>
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/sync" class="toolbar-button" title="Device Sync">
|
||||
<img src={icons.device} alt="Device Sync" />
|
||||
<span>Sync</span>
|
||||
</a>
|
||||
|
||||
<a href="/settings" class="toolbar-button" title="Settings">
|
||||
<img src={icons.computer} alt="Settings" />
|
||||
<span>Settings</span>
|
||||
|
||||
107
src/lib/services/deviceSync.ts
Normal file
107
src/lib/services/deviceSync.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Device sync service layer
|
||||
* Handles communication with Tauri backend for device synchronization
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
|
||||
export interface FileInfo {
|
||||
relativePath: string;
|
||||
size: number;
|
||||
status: 'new' | 'updated';
|
||||
}
|
||||
|
||||
export interface SyncStats {
|
||||
newFiles: number;
|
||||
updatedFiles: number;
|
||||
unchangedFiles: number;
|
||||
totalSize: number;
|
||||
}
|
||||
|
||||
export interface SyncDiff {
|
||||
filesToCopy: FileInfo[];
|
||||
stats: SyncStats;
|
||||
}
|
||||
|
||||
export interface SyncProgress {
|
||||
current: number;
|
||||
total: number;
|
||||
currentFile: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export type ProgressCallback = (progress: SyncProgress) => void;
|
||||
|
||||
/**
|
||||
* Index device and compare with library
|
||||
* Returns a diff showing which files need to be synced
|
||||
*/
|
||||
export async function indexAndCompare(
|
||||
libraryPath: string,
|
||||
devicePath: string,
|
||||
overwriteMode: string
|
||||
): Promise<SyncDiff> {
|
||||
try {
|
||||
const result = await invoke<SyncDiff>('index_and_compare', {
|
||||
libraryPath,
|
||||
devicePath,
|
||||
overwriteMode
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error indexing and comparing:', error);
|
||||
throw new Error(String(error));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync files to device with progress updates
|
||||
*/
|
||||
export async function syncToDevice(
|
||||
libraryPath: string,
|
||||
devicePath: string,
|
||||
filesToCopy: FileInfo[],
|
||||
onProgress?: ProgressCallback
|
||||
): Promise<string> {
|
||||
let unlisten: UnlistenFn | null = null;
|
||||
|
||||
try {
|
||||
// Set up progress listener
|
||||
unlisten = await listen<SyncProgress>('sync-progress', (event) => {
|
||||
if (onProgress) {
|
||||
onProgress(event.payload);
|
||||
}
|
||||
});
|
||||
|
||||
// Start sync operation
|
||||
const result = await invoke<string>('sync_to_device', {
|
||||
libraryPath,
|
||||
devicePath,
|
||||
filesToCopy
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error syncing to device:', error);
|
||||
throw new Error(String(error));
|
||||
} finally {
|
||||
// Clean up event listener
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes to human-readable string
|
||||
*/
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
81
src/lib/stores/deviceSync.ts
Normal file
81
src/lib/stores/deviceSync.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { LazyStore } from '@tauri-apps/plugin-store';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
|
||||
// Device sync settings interface
|
||||
export type OverwriteMode = 'skip' | 'different' | 'always';
|
||||
|
||||
export interface DeviceSyncSettings {
|
||||
musicPath: string | null;
|
||||
playlistsPath: string | null;
|
||||
overwriteMode: OverwriteMode;
|
||||
}
|
||||
|
||||
// Initialize the store with device-sync.json
|
||||
const store = new LazyStore('device-sync.json');
|
||||
|
||||
// Default settings
|
||||
const defaultSettings: DeviceSyncSettings = {
|
||||
musicPath: null,
|
||||
playlistsPath: null,
|
||||
overwriteMode: 'different'
|
||||
};
|
||||
|
||||
// Create a writable store for reactive UI updates
|
||||
export const deviceSyncSettings: Writable<DeviceSyncSettings> = writable(defaultSettings);
|
||||
|
||||
// Load settings from store
|
||||
export async function loadDeviceSyncSettings(): Promise<void> {
|
||||
const musicPath = await store.get<string>('musicPath');
|
||||
const playlistsPath = await store.get<string>('playlistsPath');
|
||||
const overwriteMode = await store.get<OverwriteMode>('overwriteMode');
|
||||
|
||||
deviceSyncSettings.set({
|
||||
musicPath: musicPath ?? null,
|
||||
playlistsPath: playlistsPath ?? null,
|
||||
overwriteMode: overwriteMode ?? 'different'
|
||||
});
|
||||
}
|
||||
|
||||
// Save device music path setting
|
||||
export async function setMusicPath(path: string | null): Promise<void> {
|
||||
if (path) {
|
||||
await store.set('musicPath', path);
|
||||
} else {
|
||||
await store.delete('musicPath');
|
||||
}
|
||||
await store.save();
|
||||
|
||||
deviceSyncSettings.update(s => ({
|
||||
...s,
|
||||
musicPath: path
|
||||
}));
|
||||
}
|
||||
|
||||
// Save device playlists path setting
|
||||
export async function setPlaylistsPath(path: string | null): Promise<void> {
|
||||
if (path) {
|
||||
await store.set('playlistsPath', path);
|
||||
} else {
|
||||
await store.delete('playlistsPath');
|
||||
}
|
||||
await store.save();
|
||||
|
||||
deviceSyncSettings.update(s => ({
|
||||
...s,
|
||||
playlistsPath: path
|
||||
}));
|
||||
}
|
||||
|
||||
// Save overwrite mode setting
|
||||
export async function setOverwriteMode(mode: OverwriteMode): Promise<void> {
|
||||
await store.set('overwriteMode', mode);
|
||||
await store.save();
|
||||
|
||||
deviceSyncSettings.update(s => ({
|
||||
...s,
|
||||
overwriteMode: mode
|
||||
}));
|
||||
}
|
||||
|
||||
// Initialize settings on import
|
||||
loadDeviceSyncSettings();
|
||||
608
src/routes/sync/+page.svelte
Normal file
608
src/routes/sync/+page.svelte
Normal file
@@ -0,0 +1,608 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { exists } from '@tauri-apps/plugin-fs';
|
||||
import { settings } from '$lib/stores/settings';
|
||||
import {
|
||||
deviceSyncSettings,
|
||||
loadDeviceSyncSettings,
|
||||
setMusicPath,
|
||||
setPlaylistsPath,
|
||||
setOverwriteMode,
|
||||
type OverwriteMode
|
||||
} from '$lib/stores/deviceSync';
|
||||
import {
|
||||
indexAndCompare,
|
||||
syncToDevice,
|
||||
formatBytes,
|
||||
type SyncDiff,
|
||||
type SyncProgress
|
||||
} from '$lib/services/deviceSync';
|
||||
|
||||
type ViewMode = 'sync' | 'preferences';
|
||||
|
||||
let viewMode = $state<ViewMode>('sync');
|
||||
let configured = $state(false);
|
||||
let deviceConnected = $state(false);
|
||||
let checkingConnection = $state(false);
|
||||
|
||||
// Path inputs for initial setup
|
||||
let musicPathInput = $state('');
|
||||
let playlistsPathInput = $state('');
|
||||
|
||||
// Sync state
|
||||
let indexing = $state(false);
|
||||
let syncing = $state(false);
|
||||
let syncDiff = $state<SyncDiff | null>(null);
|
||||
let syncProgress = $state<SyncProgress | null>(null);
|
||||
let error = $state<string | null>(null);
|
||||
let successMessage = $state<string | null>(null);
|
||||
let selectedFileIndex = $state<number | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
await loadDeviceSyncSettings();
|
||||
configured = !!$deviceSyncSettings.musicPath;
|
||||
|
||||
if (configured) {
|
||||
await checkDeviceConnection();
|
||||
}
|
||||
});
|
||||
|
||||
async function checkDeviceConnection() {
|
||||
if (!$deviceSyncSettings.musicPath) {
|
||||
deviceConnected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
checkingConnection = true;
|
||||
try {
|
||||
deviceConnected = await exists($deviceSyncSettings.musicPath);
|
||||
} catch (e) {
|
||||
deviceConnected = false;
|
||||
} finally {
|
||||
checkingConnection = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBrowseMusicPath() {
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: 'Select Device Music Folder'
|
||||
});
|
||||
|
||||
if (selected && typeof selected === 'string') {
|
||||
musicPathInput = selected;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBrowsePlaylistsPath() {
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: 'Select Device Playlists Folder'
|
||||
});
|
||||
|
||||
if (selected && typeof selected === 'string') {
|
||||
playlistsPathInput = selected;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveConfiguration() {
|
||||
if (!musicPathInput) {
|
||||
error = 'Please select a music folder path';
|
||||
return;
|
||||
}
|
||||
|
||||
error = null;
|
||||
await setMusicPath(musicPathInput);
|
||||
await setPlaylistsPath(playlistsPathInput || null);
|
||||
|
||||
configured = true;
|
||||
await checkDeviceConnection();
|
||||
}
|
||||
|
||||
async function handleIndexAndCompare() {
|
||||
if (!$settings.musicFolder) {
|
||||
error = 'No library music folder configured. Please set one in Settings.';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$deviceSyncSettings.musicPath) {
|
||||
error = 'No device music path configured.';
|
||||
return;
|
||||
}
|
||||
|
||||
indexing = true;
|
||||
error = null;
|
||||
successMessage = null;
|
||||
syncDiff = null;
|
||||
|
||||
try {
|
||||
await checkDeviceConnection();
|
||||
if (!deviceConnected) {
|
||||
throw new Error('Device is not connected or path does not exist');
|
||||
}
|
||||
|
||||
const result = await indexAndCompare(
|
||||
$settings.musicFolder,
|
||||
$deviceSyncSettings.musicPath,
|
||||
$deviceSyncSettings.overwriteMode
|
||||
);
|
||||
|
||||
syncDiff = result;
|
||||
successMessage = `Found ${result.filesToCopy.length} files to sync`;
|
||||
} catch (e) {
|
||||
error = 'Error indexing device: ' + (e instanceof Error ? e.message : String(e));
|
||||
syncDiff = null;
|
||||
} finally {
|
||||
indexing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSync() {
|
||||
if (!syncDiff || syncDiff.filesToCopy.length === 0) {
|
||||
error = 'No files to sync. Run Index & Compare first.';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$settings.musicFolder || !$deviceSyncSettings.musicPath) {
|
||||
error = 'Configuration error';
|
||||
return;
|
||||
}
|
||||
|
||||
syncing = true;
|
||||
error = null;
|
||||
successMessage = null;
|
||||
syncProgress = null;
|
||||
|
||||
try {
|
||||
const result = await syncToDevice(
|
||||
$settings.musicFolder,
|
||||
$deviceSyncSettings.musicPath,
|
||||
syncDiff.filesToCopy,
|
||||
(progress) => {
|
||||
syncProgress = progress;
|
||||
}
|
||||
);
|
||||
|
||||
successMessage = result;
|
||||
syncDiff = null;
|
||||
} catch (e) {
|
||||
error = 'Error syncing to device: ' + (e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
syncing = false;
|
||||
syncProgress = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateMusicPath() {
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: 'Select Device Music Folder'
|
||||
});
|
||||
|
||||
if (selected && typeof selected === 'string') {
|
||||
await setMusicPath(selected);
|
||||
await checkDeviceConnection();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdatePlaylistsPath() {
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: 'Select Device Playlists Folder'
|
||||
});
|
||||
|
||||
if (selected && typeof selected === 'string') {
|
||||
await setPlaylistsPath(selected);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileClick(index: number) {
|
||||
selectedFileIndex = index;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="sync-wrapper">
|
||||
<h2 style="padding: 8px">Device Sync</h2>
|
||||
|
||||
{#if !configured}
|
||||
<!-- Initial Configuration -->
|
||||
<section class="window config-section" style="max-width: 600px; margin: 8px;">
|
||||
<div class="title-bar">
|
||||
<div class="title-bar-text">Configure Device Paths</div>
|
||||
</div>
|
||||
<div class="window-body" style="padding: 12px;">
|
||||
<p>Select the folders on your portable device to sync music and playlists:</p>
|
||||
|
||||
<div class="field-row-stacked">
|
||||
<label for="music-path-input">Device Music Folder</label>
|
||||
<div class="input-with-button">
|
||||
<input
|
||||
id="music-path-input"
|
||||
type="text"
|
||||
bind:value={musicPathInput}
|
||||
placeholder="/Volumes/iPod/Music"
|
||||
readonly
|
||||
/>
|
||||
<button onclick={handleBrowseMusicPath}>Browse...</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row-stacked">
|
||||
<label for="playlists-path-input">Device Playlists Folder</label>
|
||||
<div class="input-with-button">
|
||||
<input
|
||||
id="playlists-path-input"
|
||||
type="text"
|
||||
bind:value={playlistsPathInput}
|
||||
placeholder="/Volumes/iPod/Playlists"
|
||||
readonly
|
||||
/>
|
||||
<button onclick={handleBrowsePlaylistsPath}>Browse...</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="error">{error}</p>
|
||||
{/if}
|
||||
|
||||
<div class="button-row">
|
||||
<button onclick={handleSaveConfiguration}>Save Configuration</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{:else if syncing}
|
||||
<div class="sync-status">
|
||||
{#if syncProgress && syncProgress.total > 0}
|
||||
<p class="progress-text">{syncProgress.current} / {syncProgress.total} files</p>
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
style="width: {(syncProgress.current / syncProgress.total) * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
{#if syncProgress.currentFile}
|
||||
<p class="current-file">{syncProgress.currentFile}</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p>Syncing...</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if indexing}
|
||||
<p style="padding: 8px;">Indexing device...</p>
|
||||
{:else if error}
|
||||
<p class="error" style="padding: 8px;">{error}</p>
|
||||
{:else}
|
||||
<section class="sync-content">
|
||||
<!-- Tabs -->
|
||||
<!--
|
||||
svelte-ignore a11y_no_noninteractive_element_to_interactive_role
|
||||
Reason: 98.css library requires <menu role="tablist"> for proper tab styling.
|
||||
-->
|
||||
<menu role="tablist">
|
||||
<li role="tab" aria-selected={viewMode === 'sync'}>
|
||||
<button onclick={() => viewMode = 'sync'}>Sync</button>
|
||||
</li>
|
||||
<li role="tab" aria-selected={viewMode === 'preferences'}>
|
||||
<button onclick={() => viewMode = 'preferences'}>Preferences</button>
|
||||
</li>
|
||||
</menu>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="window tab-content" role="tabpanel">
|
||||
<div class="window-body">
|
||||
{#if viewMode === 'sync'}
|
||||
<!-- Sync Tab -->
|
||||
<div class="sync-info">
|
||||
<p>
|
||||
<strong>Device:</strong>
|
||||
{#if checkingConnection}
|
||||
Checking...
|
||||
{:else if deviceConnected}
|
||||
Connected - {$deviceSyncSettings.musicPath}
|
||||
{:else}
|
||||
Not Connected - {$deviceSyncSettings.musicPath}
|
||||
{/if}
|
||||
</p>
|
||||
<div class="button-group">
|
||||
<button
|
||||
onclick={handleIndexAndCompare}
|
||||
disabled={indexing || syncing || !deviceConnected}
|
||||
>
|
||||
{indexing ? 'Indexing...' : 'Index & Compare'}
|
||||
</button>
|
||||
<button
|
||||
onclick={handleSync}
|
||||
disabled={!syncDiff || syncDiff.filesToCopy.length === 0 || syncing || !deviceConnected}
|
||||
>
|
||||
{syncing ? 'Syncing...' : 'Sync to Device'}
|
||||
</button>
|
||||
</div>
|
||||
{#if successMessage}
|
||||
<p class="success">{successMessage}</p>
|
||||
{/if}
|
||||
{#if syncDiff}
|
||||
<p>
|
||||
New: {syncDiff.stats.newFiles} | Updated: {syncDiff.stats.updatedFiles} |
|
||||
Unchanged: {syncDiff.stats.unchangedFiles} |
|
||||
Total: {formatBytes(syncDiff.stats.totalSize)}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if syncDiff && syncDiff.filesToCopy.length > 0}
|
||||
<div class="sunken-panel table-container">
|
||||
<table class="interactive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File</th>
|
||||
<th>Status</th>
|
||||
<th>Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each syncDiff.filesToCopy as file, i}
|
||||
<tr
|
||||
class:highlighted={selectedFileIndex === i}
|
||||
onclick={() => handleFileClick(i)}
|
||||
>
|
||||
<td>{file.relativePath}</td>
|
||||
<td>{file.status}</td>
|
||||
<td class="size-cell">{formatBytes(file.size)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if viewMode === 'preferences'}
|
||||
<!-- Preferences Tab -->
|
||||
<div class="prefs-container">
|
||||
<fieldset>
|
||||
<legend>Device Paths</legend>
|
||||
<div class="field-row-stacked">
|
||||
<label>Device Music Path</label>
|
||||
<div class="path-display">
|
||||
<code>{$deviceSyncSettings.musicPath || 'Not set'}</code>
|
||||
<button onclick={handleUpdateMusicPath}>Change...</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row-stacked">
|
||||
<label>Device Playlists Path</label>
|
||||
<div class="path-display">
|
||||
<code>{$deviceSyncSettings.playlistsPath || 'Not set'}</code>
|
||||
<button onclick={handleUpdatePlaylistsPath}>Change...</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset style="margin-top: 16px;">
|
||||
<legend>When file exists on device</legend>
|
||||
<div class="field-row">
|
||||
<input
|
||||
type="radio"
|
||||
id="mode-skip"
|
||||
name="overwrite-mode"
|
||||
value="skip"
|
||||
bind:group={$deviceSyncSettings.overwriteMode}
|
||||
onchange={() => setOverwriteMode($deviceSyncSettings.overwriteMode)}
|
||||
/>
|
||||
<label for="mode-skip">Skip (don't overwrite)</label>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<input
|
||||
type="radio"
|
||||
id="mode-different"
|
||||
name="overwrite-mode"
|
||||
value="different"
|
||||
bind:group={$deviceSyncSettings.overwriteMode}
|
||||
onchange={() => setOverwriteMode($deviceSyncSettings.overwriteMode)}
|
||||
/>
|
||||
<label for="mode-different">Overwrite if different size</label>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<input
|
||||
type="radio"
|
||||
id="mode-always"
|
||||
name="overwrite-mode"
|
||||
value="always"
|
||||
bind:group={$deviceSyncSettings.overwriteMode}
|
||||
onchange={() => setOverwriteMode($deviceSyncSettings.overwriteMode)}
|
||||
/>
|
||||
<label for="mode-always">Always overwrite</label>
|
||||
</div>
|
||||
|
||||
<p class="help-text">
|
||||
System files and _temp folders are always skipped.
|
||||
</p>
|
||||
</fieldset>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sync-wrapper {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sync-status {
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
||||
.sync-status p {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: #c0c0c0;
|
||||
border: 2px inset #808080;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #000080, #0000ff);
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.current-file {
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
color: #808080;
|
||||
margin: 4px 0 0 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.field-row-stacked {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.input-with-button {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-with-button input {
|
||||
flex: 1;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.sync-content {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
margin-top: -2px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.window-body {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sync-info {
|
||||
padding: 16px 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sync-info p {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.size-cell {
|
||||
text-align: right;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.prefs-container {
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.path-display {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.path-display code {
|
||||
flex: 1;
|
||||
padding: 4px 8px;
|
||||
background: var(--button-shadow, #2a2a2a);
|
||||
border: 1px solid var(--button-highlight, #606060);
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
margin: 8px 0 0 0;
|
||||
font-size: 11px;
|
||||
color: #808080;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user