initial commit with full project
This commit is contained in:
318
src/lib/components/CompressPresets.svelte
Normal file
318
src/lib/components/CompressPresets.svelte
Normal file
@@ -0,0 +1,318 @@
|
||||
<script lang="ts">
|
||||
import { app } from '$lib/stores/app.svelte';
|
||||
|
||||
interface Props {
|
||||
onAdvancedClick?: () => void;
|
||||
showAdvanced?: boolean;
|
||||
}
|
||||
|
||||
let { onAdvancedClick, showAdvanced = false }: Props = $props();
|
||||
|
||||
const presets = [8, 25, 50, 100];
|
||||
let customActive = $state(false);
|
||||
let customValue = $state(8);
|
||||
let holdTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
let activeIdx = $derived.by(() => {
|
||||
if (app.selectedPreset === null) return -1;
|
||||
return presets.indexOf(app.selectedPreset);
|
||||
});
|
||||
|
||||
function handlePresetClick(mb: number) {
|
||||
customActive = false;
|
||||
if (app.selectedPreset === mb) {
|
||||
app.clearPreset();
|
||||
} else {
|
||||
app.setPreset(mb);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCustomClick() {
|
||||
if (customActive) {
|
||||
customActive = false;
|
||||
app.clearPreset();
|
||||
return;
|
||||
}
|
||||
customActive = true;
|
||||
customValue = 8;
|
||||
app.setCustomSize(8);
|
||||
}
|
||||
|
||||
function adjustValue(delta: number) {
|
||||
customValue = Math.max(0.5, Math.round((customValue + delta) * 10) / 10);
|
||||
app.setCustomSize(customValue);
|
||||
}
|
||||
|
||||
function startHold(delta: number) {
|
||||
adjustValue(delta);
|
||||
let speed = 200;
|
||||
let count = 0;
|
||||
holdTimer = setInterval(() => {
|
||||
adjustValue(delta);
|
||||
count++;
|
||||
// accelerate after holding
|
||||
if (count > 5 && speed > 50) {
|
||||
speed = 80;
|
||||
if (holdTimer) clearInterval(holdTimer);
|
||||
holdTimer = setInterval(() => adjustValue(delta), speed);
|
||||
}
|
||||
}, speed);
|
||||
}
|
||||
|
||||
function stopHold() {
|
||||
if (holdTimer) {
|
||||
clearInterval(holdTimer);
|
||||
holdTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
let isActive = $derived(app.selectedPreset !== null || customActive);
|
||||
</script>
|
||||
|
||||
<div class="cp" class:cp--active={isActive}>
|
||||
<div class="cp-header">
|
||||
<span class="cp-label">Compress to:</span>
|
||||
<!-- always rendered to prevent layout shift, visibility toggled -->
|
||||
<button
|
||||
type="button"
|
||||
class="cp-adv-btn"
|
||||
style="visibility: {showAdvanced ? 'visible' : 'hidden'}"
|
||||
onclick={onAdvancedClick}
|
||||
title="Advanced options"
|
||||
>
|
||||
<i class="ti ti-adjustments-horizontal" style="font-size: 14px"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cp-row">
|
||||
{#each presets as mb, i}
|
||||
<button
|
||||
type="button"
|
||||
class="cp-btn"
|
||||
class:cp-btn--active={activeIdx === i}
|
||||
onclick={() => handlePresetClick(mb)}
|
||||
>
|
||||
{mb}<span class="cp-unit">MB</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
{#if customActive}
|
||||
<!-- custom spinner replaces the Custom button -->
|
||||
<div class="cp-spinner">
|
||||
<button
|
||||
type="button"
|
||||
class="cp-spin-btn"
|
||||
onpointerdown={() => startHold(-1)}
|
||||
onpointerup={stopHold}
|
||||
onpointerleave={stopHold}
|
||||
>
|
||||
<i class="ti ti-minus" style="font-size: 12px"></i>
|
||||
</button>
|
||||
<div class="cp-spin-value">
|
||||
<input
|
||||
type="number"
|
||||
class="cp-spin-input"
|
||||
bind:value={customValue}
|
||||
oninput={() => { if (customValue > 0) app.setCustomSize(customValue); }}
|
||||
onkeydown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur(); }}
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
/>
|
||||
<span class="cp-spin-unit">MB</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="cp-spin-btn"
|
||||
onpointerdown={() => startHold(1)}
|
||||
onpointerup={stopHold}
|
||||
onpointerleave={stopHold}
|
||||
>
|
||||
<i class="ti ti-plus" style="font-size: 12px"></i>
|
||||
</button>
|
||||
<button type="button" class="cp-spin-close" onclick={handleCustomClick}>
|
||||
<i class="ti ti-x" style="font-size: 11px"></i>
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="cp-btn cp-btn--custom"
|
||||
onclick={handleCustomClick}
|
||||
>
|
||||
<i class="ti ti-pencil" style="font-size: 12px"></i>
|
||||
Custom
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cp {
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: transparent;
|
||||
transition: background var(--transition-default);
|
||||
}
|
||||
.cp--active {
|
||||
background: rgba(5, 150, 105, 0.06);
|
||||
}
|
||||
|
||||
.cp-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.cp-label {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
font-size: var(--text-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.cp-adv-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-bg-surface);
|
||||
color: var(--color-text-disabled);
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-fast), border-color var(--transition-fast), background var(--transition-fast);
|
||||
}
|
||||
.cp-adv-btn:hover {
|
||||
color: var(--color-accent-compress);
|
||||
border-color: var(--color-accent-compress);
|
||||
background: rgba(5, 150, 105, 0.06);
|
||||
}
|
||||
|
||||
.cp-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.cp-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
padding: 8px 0;
|
||||
flex: 1;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-bg-surface);
|
||||
color: var(--color-text-secondary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast), color var(--transition-fast),
|
||||
border-color var(--transition-fast), box-shadow var(--transition-default);
|
||||
}
|
||||
.cp-btn:hover:not(.cp-btn--active) {
|
||||
border-color: var(--color-accent-compress);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.cp-btn--active {
|
||||
background: var(--color-accent-compress);
|
||||
border-color: var(--color-accent-compress);
|
||||
color: white;
|
||||
box-shadow: var(--shadow-glow-compress);
|
||||
}
|
||||
.cp-btn--custom {
|
||||
font-family: var(--font-body);
|
||||
font-weight: 600;
|
||||
font-size: var(--text-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
gap: 4px;
|
||||
}
|
||||
.cp-unit {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.cp-spinner {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-accent-compress);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-bg-surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
.cp-spin-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
align-self: stretch;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-accent-compress);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast);
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.cp-spin-btn:hover {
|
||||
background: rgba(5, 150, 105, 0.1);
|
||||
}
|
||||
.cp-spin-btn:active {
|
||||
background: rgba(5, 150, 105, 0.2);
|
||||
}
|
||||
.cp-spin-value {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
padding: 8px 0;
|
||||
border-left: 1px solid var(--color-border);
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
.cp-spin-input {
|
||||
width: 48px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
color: var(--color-accent-compress);
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
.cp-spin-input::-webkit-outer-spin-button,
|
||||
.cp-spin-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
.cp-spin-unit {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
.cp-spin-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
align-self: stretch;
|
||||
border: none;
|
||||
border-left: 1px solid var(--color-border);
|
||||
background: none;
|
||||
color: var(--color-text-disabled);
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
.cp-spin-close:hover {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user