initial commit with full project

This commit is contained in:
2026-04-26 17:50:04 +03:00
commit 53044e7d40
68 changed files with 34115 additions and 0 deletions

View 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>