mirror of
https://github.com/xpltdco/media-rip.git
synced 2026-04-03 02:53:58 -06:00
Settings page: single Save, clean flow
One Save Settings button covers all configuration: - Welcome message - Default output formats (video/audio) - Privacy mode toggle + retention hours Below the save area, separated by dividers: - Manual Purge (immediate action, Sure? gate) - Change Password (immediate action, own button) Settings fields have subtle bottom borders for visual rhythm. No section headings — the flow reads naturally top-to-bottom. Removed redundant privacySaved ref and savePrivacy function.
This commit is contained in:
parent
fe45fdce50
commit
0df9573caa
1 changed files with 100 additions and 111 deletions
|
|
@ -23,7 +23,6 @@ const defaultAudioFormat = ref('auto')
|
||||||
const settingsSaved = ref(false)
|
const settingsSaved = ref(false)
|
||||||
const privacyMode = ref(false)
|
const privacyMode = ref(false)
|
||||||
const privacyRetentionHours = ref(24)
|
const privacyRetentionHours = ref(24)
|
||||||
const privacySaved = ref(false)
|
|
||||||
const purgeConfirming = ref(false)
|
const purgeConfirming = ref(false)
|
||||||
let purgeConfirmTimer: ReturnType<typeof setTimeout> | null = null
|
let purgeConfirmTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
|
@ -67,30 +66,19 @@ async function switchTab(tab: typeof activeTab.value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSettings() {
|
async function saveAllSettings() {
|
||||||
settingsSaved.value = false
|
settingsSaved.value = false
|
||||||
const ok = await store.updateSettings({
|
const ok = await store.updateSettings({
|
||||||
welcome_message: welcomeMessage.value,
|
welcome_message: welcomeMessage.value,
|
||||||
default_video_format: defaultVideoFormat.value,
|
default_video_format: defaultVideoFormat.value,
|
||||||
default_audio_format: defaultAudioFormat.value,
|
default_audio_format: defaultAudioFormat.value,
|
||||||
})
|
|
||||||
if (ok) {
|
|
||||||
await configStore.loadConfig()
|
|
||||||
settingsSaved.value = true
|
|
||||||
setTimeout(() => { settingsSaved.value = false }, 3000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function savePrivacy() {
|
|
||||||
privacySaved.value = false
|
|
||||||
const ok = await store.updateSettings({
|
|
||||||
privacy_mode: privacyMode.value,
|
privacy_mode: privacyMode.value,
|
||||||
privacy_retention_hours: privacyRetentionHours.value,
|
privacy_retention_hours: privacyRetentionHours.value,
|
||||||
})
|
})
|
||||||
if (ok) {
|
if (ok) {
|
||||||
await configStore.loadConfig()
|
await configStore.loadConfig()
|
||||||
privacySaved.value = true
|
settingsSaved.value = true
|
||||||
setTimeout(() => { privacySaved.value = false }, 3000)
|
setTimeout(() => { settingsSaved.value = false }, 3000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,10 +259,8 @@ function formatFilesize(bytes: number | null): string {
|
||||||
|
|
||||||
<!-- Settings tab -->
|
<!-- Settings tab -->
|
||||||
<div v-if="activeTab === 'settings'" class="tab-content">
|
<div v-if="activeTab === 'settings'" class="tab-content">
|
||||||
<!-- Section: Appearance & Defaults -->
|
<!-- All configurable settings in one form -->
|
||||||
<div class="settings-section">
|
<div class="settings-form">
|
||||||
<h3 class="section-heading">Appearance & Defaults</h3>
|
|
||||||
|
|
||||||
<div class="settings-field">
|
<div class="settings-field">
|
||||||
<label for="welcome-msg">Welcome Message</label>
|
<label for="welcome-msg">Welcome Message</label>
|
||||||
<p class="field-hint">Displayed above the URL input on the main page. Leave empty to hide.</p>
|
<p class="field-hint">Displayed above the URL input on the main page. Leave empty to hide.</p>
|
||||||
|
|
@ -287,7 +273,7 @@ function formatFilesize(bytes: number | null): string {
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-field" style="margin-top: var(--space-lg);">
|
<div class="settings-field">
|
||||||
<label>Default Output Formats</label>
|
<label>Default Output Formats</label>
|
||||||
<p class="field-hint">When "Auto" is selected, files are converted to these formats instead of the native container.</p>
|
<p class="field-hint">When "Auto" is selected, files are converted to these formats instead of the native container.</p>
|
||||||
<div class="format-defaults">
|
<div class="format-defaults">
|
||||||
|
|
@ -313,20 +299,6 @@ function formatFilesize(bytes: number | null): string {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-actions">
|
|
||||||
<button @click="saveSettings" :disabled="store.isLoading" class="btn-save">
|
|
||||||
{{ store.isLoading ? 'Saving…' : 'Save' }}
|
|
||||||
</button>
|
|
||||||
<span v-if="settingsSaved" class="save-confirm">✓ Saved</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="settings-divider" />
|
|
||||||
|
|
||||||
<!-- Section: Privacy & Data -->
|
|
||||||
<div class="settings-section">
|
|
||||||
<h3 class="section-heading">Privacy & Data</h3>
|
|
||||||
|
|
||||||
<div class="settings-field">
|
<div class="settings-field">
|
||||||
<label class="toggle-label">
|
<label class="toggle-label">
|
||||||
<span>Privacy Mode</span>
|
<span>Privacy Mode</span>
|
||||||
|
|
@ -357,93 +329,90 @@ function formatFilesize(bytes: number | null): string {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-actions">
|
<div class="settings-actions settings-save-row">
|
||||||
<button @click="savePrivacy" :disabled="store.isLoading" class="btn-save">
|
<button @click="saveAllSettings" :disabled="store.isLoading" class="btn-save">
|
||||||
{{ store.isLoading ? 'Saving…' : 'Save' }}
|
{{ store.isLoading ? 'Saving…' : 'Save Settings' }}
|
||||||
</button>
|
</button>
|
||||||
<span v-if="privacySaved" class="save-confirm">✓ Saved</span>
|
<span v-if="settingsSaved" class="save-confirm">✓ Saved</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="field-hint">
|
||||||
|
Settings are applied immediately but reset on server restart.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings-field" style="margin-top: var(--space-lg);">
|
<hr class="settings-divider" />
|
||||||
<label>Manual Purge</label>
|
|
||||||
<p class="field-hint">
|
<!-- Actions (not "settings" — these are immediate operations) -->
|
||||||
Immediately clear all completed and failed downloads — removes
|
<div class="settings-field">
|
||||||
database records, files from disk, and orphaned sessions.
|
<label>Manual Purge</label>
|
||||||
Active downloads are never affected.
|
<p class="field-hint">
|
||||||
</p>
|
Immediately clear all completed and failed downloads — removes
|
||||||
<button
|
database records, files from disk, and orphaned sessions.
|
||||||
@click="handlePurgeClick"
|
Active downloads are never affected.
|
||||||
:disabled="store.isLoading"
|
</p>
|
||||||
class="btn-purge"
|
<button
|
||||||
:class="{ 'btn-confirm': purgeConfirming }"
|
@click="handlePurgeClick"
|
||||||
>
|
:disabled="store.isLoading"
|
||||||
{{ store.isLoading ? 'Purging…' : purgeConfirming ? 'Sure?' : 'Run Purge Now' }}
|
class="btn-purge"
|
||||||
</button>
|
:class="{ 'btn-confirm': purgeConfirming }"
|
||||||
<div v-if="store.purgeResult" class="purge-result">
|
>
|
||||||
<p>{{ store.purgeResult.rows_deleted }} jobs removed</p>
|
{{ store.isLoading ? 'Purging…' : purgeConfirming ? 'Sure?' : 'Run Purge Now' }}
|
||||||
<p>{{ store.purgeResult.files_deleted }} files deleted</p>
|
</button>
|
||||||
<p v-if="store.purgeResult.sessions_deleted">{{ store.purgeResult.sessions_deleted }} sessions cleared</p>
|
<div v-if="store.purgeResult" class="purge-result">
|
||||||
<p v-if="store.purgeResult.active_skipped">{{ store.purgeResult.active_skipped }} active jobs skipped</p>
|
<p>{{ store.purgeResult.rows_deleted }} jobs removed</p>
|
||||||
</div>
|
<p>{{ store.purgeResult.files_deleted }} files deleted</p>
|
||||||
|
<p v-if="store.purgeResult.sessions_deleted">{{ store.purgeResult.sessions_deleted }} sessions cleared</p>
|
||||||
|
<p v-if="store.purgeResult.active_skipped">{{ store.purgeResult.active_skipped }} active jobs skipped</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="settings-divider" />
|
<hr class="settings-divider" />
|
||||||
|
|
||||||
<!-- Section: Security -->
|
<div class="settings-field">
|
||||||
<div class="settings-section">
|
<label>Change Password</label>
|
||||||
<h3 class="section-heading">Security</h3>
|
<p class="field-hint">Takes effect immediately but resets on server restart.</p>
|
||||||
|
<div class="password-fields">
|
||||||
<div class="settings-field">
|
<input
|
||||||
<label>Change Password</label>
|
v-model="currentPassword"
|
||||||
<p class="field-hint">Update the admin password. Takes effect immediately but resets on server restart.</p>
|
type="password"
|
||||||
<div class="password-fields">
|
placeholder="Current password"
|
||||||
<input
|
autocomplete="current-password"
|
||||||
v-model="currentPassword"
|
class="settings-input"
|
||||||
type="password"
|
/>
|
||||||
placeholder="Current password"
|
<input
|
||||||
autocomplete="current-password"
|
v-model="newPassword"
|
||||||
class="settings-input"
|
type="password"
|
||||||
/>
|
placeholder="New password"
|
||||||
<input
|
autocomplete="new-password"
|
||||||
v-model="newPassword"
|
class="settings-input"
|
||||||
type="password"
|
/>
|
||||||
placeholder="New password"
|
<input
|
||||||
autocomplete="new-password"
|
v-model="confirmPassword"
|
||||||
class="settings-input"
|
type="password"
|
||||||
/>
|
placeholder="Confirm new password"
|
||||||
<input
|
autocomplete="new-password"
|
||||||
v-model="confirmPassword"
|
class="settings-input"
|
||||||
type="password"
|
@keydown.enter="changePassword"
|
||||||
placeholder="Confirm new password"
|
/>
|
||||||
autocomplete="new-password"
|
<span
|
||||||
class="settings-input"
|
v-if="confirmPassword && newPassword && confirmPassword !== newPassword"
|
||||||
@keydown.enter="changePassword"
|
class="password-mismatch"
|
||||||
/>
|
>
|
||||||
<span
|
Passwords don't match
|
||||||
v-if="confirmPassword && newPassword && confirmPassword !== newPassword"
|
</span>
|
||||||
class="password-mismatch"
|
</div>
|
||||||
>
|
<div class="settings-actions" style="margin-top: var(--space-sm);">
|
||||||
Passwords don't match
|
<button
|
||||||
</span>
|
@click="changePassword"
|
||||||
</div>
|
:disabled="!canChangePassword || changingPassword"
|
||||||
<div class="settings-actions" style="margin-top: var(--space-sm);">
|
class="btn-save"
|
||||||
<button
|
>
|
||||||
@click="changePassword"
|
{{ changingPassword ? 'Changing…' : 'Change Password' }}
|
||||||
:disabled="!canChangePassword || changingPassword"
|
</button>
|
||||||
class="btn-save"
|
<span v-if="passwordChanged" class="save-confirm">✓ Password changed</span>
|
||||||
>
|
<span v-if="passwordError" class="password-error">{{ passwordError }}</span>
|
||||||
{{ changingPassword ? 'Changing…' : 'Change Password' }}
|
|
||||||
</button>
|
|
||||||
<span v-if="passwordChanged" class="save-confirm">✓ Password changed</span>
|
|
||||||
<span v-if="passwordError" class="password-error">{{ passwordError }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="field-hint" style="margin-top: var(--space-lg);">
|
|
||||||
All settings are applied immediately but reset on server restart.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -592,6 +561,26 @@ h3 {
|
||||||
margin-bottom: var(--space-sm);
|
margin-bottom: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-form .settings-field {
|
||||||
|
padding-bottom: var(--space-md);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-form .settings-field:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-save-row {
|
||||||
|
padding-top: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-md);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue