Files
embycovers/homescreen_editor/lib/SyncPanel.svelte
T

332 lines
7.7 KiB
Svelte
Raw Normal View History

2026-06-08 21:58:16 +12:00
<script>
import { createEventDispatcher } from 'svelte';
import { getSectionTypeLabel, getGenreNames } from '$lib/constants.js';
export let users = [];
const dispatch = createEventDispatcher();
let sourceUserId = null;
let targetUserIds = [];
let selectedSectionIndices = [];
let syncMode = 'replace'; // 'replace' = overwrite all, 'append' = add to end, 'selected' = only selected sections
$: sourceUser = users.find((u) => u.id === sourceUserId);
$: sourceSections = sourceUser?.sections || [];
$: availableTargets = users.filter((u) => u.id !== sourceUserId && u.sections?.length >= 0);
function toggleTarget(id) {
if (targetUserIds.includes(id)) {
targetUserIds = targetUserIds.filter((t) => t !== id);
} else {
targetUserIds = [...targetUserIds, id];
}
}
function selectAllTargets() {
targetUserIds = availableTargets.map((u) => u.id);
}
function deselectAllTargets() {
targetUserIds = [];
}
function toggleSectionIndex(idx) {
if (selectedSectionIndices.includes(idx)) {
selectedSectionIndices = selectedSectionIndices.filter((i) => i !== idx);
} else {
selectedSectionIndices = [...selectedSectionIndices, idx];
}
}
function doSync() {
if (!sourceUser || targetUserIds.length === 0) return;
let sectionsToSync;
if (syncMode === 'selected') {
sectionsToSync = selectedSectionIndices.map((i) => sourceSections[i]).filter(Boolean);
} else {
sectionsToSync = [...sourceSections];
}
dispatch('sync', {
sourceUserId,
targetUserIds,
sections: sectionsToSync,
mode: syncMode
});
}
</script>
<div class="sync-panel">
<h3>Sync sections between users</h3>
<div class="sync-step">
<span class="step-label">1. Source user</span>
<select bind:value={sourceUserId} class="select-input">
<option value={null}>Select source user...</option>
{#each users.filter((u) => u.sections?.length > 0) as user}
<option value={user.id}>{user.name} ({user.sections.length} sections)</option>
{/each}
</select>
</div>
{#if sourceUser}
<div class="sync-step">
<span class="step-label">2. Sync mode</span>
<div class="mode-group">
<label class="mode-option" class:active={syncMode === 'replace'}>
<input type="radio" bind:group={syncMode} value="replace" />
<div>
<strong>Replace all</strong>
<span class="mode-desc">Overwrite target's sections entirely</span>
</div>
</label>
<label class="mode-option" class:active={syncMode === 'append'}>
<input type="radio" bind:group={syncMode} value="append" />
<div>
<strong>Append all</strong>
<span class="mode-desc">Add source sections to end of target</span>
</div>
</label>
<label class="mode-option" class:active={syncMode === 'selected'}>
<input type="radio" bind:group={syncMode} value="selected" />
<div>
<strong>Selected only</strong>
<span class="mode-desc">Pick specific sections to sync</span>
</div>
</label>
</div>
</div>
{#if syncMode === 'selected'}
<div class="sync-step">
<span class="step-label">Sections to sync</span>
<div class="section-pick-list">
{#each sourceSections as section, idx}
<label class="section-pick" class:selected={selectedSectionIndices.includes(idx)}>
<input
type="checkbox"
checked={selectedSectionIndices.includes(idx)}
on:change={() => toggleSectionIndex(idx)}
/>
<span class="pick-name">{section.CustomName || section.Name || '(unnamed)'}</span>
<span class="pick-type">{getSectionTypeLabel(section.SectionType)}</span>
</label>
{/each}
</div>
</div>
{/if}
<div class="sync-step">
<div class="step-label-row">
<span class="step-label">3. Target users</span>
<div class="select-btns">
<button class="link-btn" on:click={selectAllTargets}>All</button>
<button class="link-btn" on:click={deselectAllTargets}>None</button>
</div>
</div>
<div class="target-list">
{#each availableTargets as user}
<label class="target-option" class:selected={targetUserIds.includes(user.id)}>
<input
type="checkbox"
checked={targetUserIds.includes(user.id)}
on:change={() => toggleTarget(user.id)}
/>
<span class="target-name">{user.name}</span>
<span class="target-count">{user.sections?.length || 0} sections</span>
</label>
{/each}
</div>
</div>
<button
class="sync-btn"
disabled={targetUserIds.length === 0 ||
(syncMode === 'selected' && selectedSectionIndices.length === 0)}
on:click={doSync}
>
Sync {syncMode === 'selected' ? `${selectedSectionIndices.length} section(s)` : 'all sections'}
to {targetUserIds.length} user{targetUserIds.length !== 1 ? 's' : ''}
</button>
{/if}
</div>
<style>
.sync-panel {
padding: 0;
}
h3 {
font-size: 16px;
font-weight: 800;
margin: 0 0 18px;
color: var(--text);
}
.sync-step {
margin-bottom: 16px;
}
.step-label {
font-size: 11px;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
display: block;
margin-bottom: 6px;
}
.step-label-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.select-input {
width: 100%;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: #12151b;
color: var(--text);
font-size: 13px;
font-family: inherit;
}
.mode-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.mode-option {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: #111419;
cursor: pointer;
transition: all 0.12s;
}
.mode-option.active {
border-color: var(--border-strong);
background: var(--surface-active);
}
.mode-option input {
margin-top: 2px;
}
.mode-option strong {
font-size: 13px;
color: var(--text);
display: block;
}
.mode-desc {
font-size: 12px;
color: var(--text-muted);
}
.section-pick-list {
display: flex;
flex-direction: column;
gap: 3px;
max-height: 200px;
overflow-y: auto;
}
.section-pick {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 10px;
border: 1px solid transparent;
cursor: pointer;
font-size: 13px;
}
.section-pick:hover {
background: var(--surface-hover);
}
.section-pick.selected {
background: var(--surface-active);
border: 1px solid var(--border-strong);
}
.pick-name {
flex: 1;
color: var(--text);
}
.pick-type {
font-size: 11px;
color: var(--text-muted);
}
.target-list {
display: flex;
flex-direction: column;
gap: 3px;
}
.target-option {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 10px;
border: 1px solid transparent;
cursor: pointer;
font-size: 13px;
}
.target-option:hover {
background: var(--surface-hover);
}
.target-option.selected {
background: var(--surface-active);
border: 1px solid var(--border-strong);
}
.target-name {
flex: 1;
color: var(--text);
}
.target-count {
font-size: 11px;
color: var(--text-muted);
}
.link-btn {
all: unset;
cursor: pointer;
font-size: 12px;
color: var(--accent);
}
.link-btn:hover {
text-decoration: underline;
}
.sync-btn {
all: unset;
cursor: pointer;
display: block;
width: 100%;
text-align: center;
padding: 12px;
border-radius: 10px;
background: var(--accent);
border: 1px solid rgba(42, 215, 239, 0.35);
color: #031014;
font-size: 14px;
font-weight: 600;
font-family: inherit;
box-sizing: border-box;
transition: opacity 0.12s, background 0.12s ease;
}
.sync-btn:hover:not(:disabled) {
background: #35d2ea;
}
.sync-btn:disabled {
opacity: 0.4;
cursor: default;
}
.select-btns {
display: flex;
gap: 10px;
}
</style>