|  | 
|  | 1 | +<script lang="ts"> | 
|  | 2 | +    import ClipRangeSlider from "./ClipRangeSlider.svelte"; | 
|  | 3 | +
 | 
|  | 4 | +    let { | 
|  | 5 | +        metaLoading, | 
|  | 6 | +        metaError, | 
|  | 7 | +        metadata, | 
|  | 8 | +        clipStart = $bindable(), | 
|  | 9 | +        clipEnd = $bindable() | 
|  | 10 | +    } = $props<{ | 
|  | 11 | +        metaLoading: boolean; | 
|  | 12 | +        metaError: string | null; | 
|  | 13 | +        metadata: { title?: string; author?: string; duration?: number } | null; | 
|  | 14 | +        clipStart: number; | 
|  | 15 | +        clipEnd: number; | 
|  | 16 | +    }>(); | 
|  | 17 | +
 | 
|  | 18 | +    function formatTime(seconds: number) { | 
|  | 19 | +        const m = Math.floor(seconds / 60); | 
|  | 20 | +        const s = (seconds % 60).toFixed(3).padStart(6, '0'); | 
|  | 21 | +        return `${m}:${s}`; | 
|  | 22 | +    } | 
|  | 23 | +</script> | 
|  | 24 | + | 
|  | 25 | +<div class="clip-controls"> | 
|  | 26 | +    {#if metaLoading} | 
|  | 27 | +        <div class="loading-state"> | 
|  | 28 | +            <div class="loading-spinner"></div> | 
|  | 29 | +            <span>Loading video metadata...</span> | 
|  | 30 | +        </div> | 
|  | 31 | +    {:else if metaError} | 
|  | 32 | +        <div class="error-state"> | 
|  | 33 | +            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | 
|  | 34 | +                <circle cx="12" cy="12" r="10"></circle> | 
|  | 35 | +                <line x1="15" y1="9" x2="9" y2="15"></line> | 
|  | 36 | +                <line x1="9" y1="9" x2="15" y2="15"></line> | 
|  | 37 | +            </svg> | 
|  | 38 | +            <span>{metaError}</span> | 
|  | 39 | +        </div> | 
|  | 40 | +    {:else if metadata} | 
|  | 41 | +        <div class="clip-metadata"> | 
|  | 42 | +            <div class="metadata-header"> | 
|  | 43 | +                <h4>Clip</h4> | 
|  | 44 | +                <div class="duration-badge"> | 
|  | 45 | +                    {typeof metadata.duration === 'number' ? formatTime(metadata.duration) : 'Unknown'} | 
|  | 46 | +                </div> | 
|  | 47 | +            </div> | 
|  | 48 | +            <div class="video-info"> | 
|  | 49 | +                <div class="video-title" title={metadata.title}> | 
|  | 50 | +                    {metadata.title} | 
|  | 51 | +                </div> | 
|  | 52 | +                {#if metadata.author} | 
|  | 53 | +                    <div class="video-author"> | 
|  | 54 | +                        by {metadata.author} | 
|  | 55 | +                    </div> | 
|  | 56 | +                {/if} | 
|  | 57 | +            </div> | 
|  | 58 | +        </div> | 
|  | 59 | +         | 
|  | 60 | +        <ClipRangeSlider | 
|  | 61 | +            min={0} | 
|  | 62 | +            max={metadata.duration || 0} | 
|  | 63 | +            step={0.001} | 
|  | 64 | +            bind:start={clipStart} | 
|  | 65 | +            bind:end={clipEnd} | 
|  | 66 | +        /> | 
|  | 67 | +         | 
|  | 68 | +        <div class="clip-time-display"> | 
|  | 69 | +            <div class="time-indicators"> | 
|  | 70 | +                <span class="start-time">{formatTime(clipStart)}</span> | 
|  | 71 | +                <span class="duration-selected"> | 
|  | 72 | +                    {formatTime(Math.max(0, clipEnd - clipStart))} selected | 
|  | 73 | +                </span> | 
|  | 74 | +                <span class="end-time">{formatTime(clipEnd)}</span> | 
|  | 75 | +            </div> | 
|  | 76 | +        </div> | 
|  | 77 | +    {/if} | 
|  | 78 | +</div> | 
|  | 79 | + | 
|  | 80 | +<style> | 
|  | 81 | +    .clip-controls { | 
|  | 82 | +        margin-top: 12px; | 
|  | 83 | +        padding: 16px; | 
|  | 84 | +        background: var(--button); | 
|  | 85 | +        border-radius: var(--border-radius); | 
|  | 86 | +        border: 1px solid var(--button-stroke); | 
|  | 87 | +        animation: slideIn 0.3s cubic-bezier(0.2, 0, 0, 1); | 
|  | 88 | +    } | 
|  | 89 | +
 | 
|  | 90 | +    .loading-state { | 
|  | 91 | +        display: flex; | 
|  | 92 | +        align-items: center; | 
|  | 93 | +        gap: 12px; | 
|  | 94 | +        padding: 16px 0; | 
|  | 95 | +        color: var(--gray); | 
|  | 96 | +        font-size: 14px; | 
|  | 97 | +    } | 
|  | 98 | +
 | 
|  | 99 | +    .loading-spinner { | 
|  | 100 | +        width: 16px; | 
|  | 101 | +        height: 16px; | 
|  | 102 | +        border: 2px solid var(--button-hover); | 
|  | 103 | +        border-top: 2px solid var(--secondary); | 
|  | 104 | +        border-radius: 50%; | 
|  | 105 | +        animation: spin 1s linear infinite; | 
|  | 106 | +    } | 
|  | 107 | +
 | 
|  | 108 | +    .error-state { | 
|  | 109 | +        display: flex; | 
|  | 110 | +        align-items: center; | 
|  | 111 | +        gap: 8px; | 
|  | 112 | +        padding: 12px 16px; | 
|  | 113 | +        background: rgba(237, 34, 54, 0.1); | 
|  | 114 | +        border: 1px solid rgba(237, 34, 54, 0.2); | 
|  | 115 | +        border-radius: 8px; | 
|  | 116 | +        color: var(--red); | 
|  | 117 | +        font-size: 13px; | 
|  | 118 | +        font-weight: 500; | 
|  | 119 | +    } | 
|  | 120 | +
 | 
|  | 121 | +    .clip-metadata { | 
|  | 122 | +        margin-bottom: 16px; | 
|  | 123 | +    } | 
|  | 124 | +
 | 
|  | 125 | +    .metadata-header { | 
|  | 126 | +        display: flex; | 
|  | 127 | +        align-items: center; | 
|  | 128 | +        justify-content: space-between; | 
|  | 129 | +        margin-bottom: 12px; | 
|  | 130 | +        padding-bottom: 8px; | 
|  | 131 | +    } | 
|  | 132 | +
 | 
|  | 133 | +    .metadata-header h4 { | 
|  | 134 | +        margin: 0; | 
|  | 135 | +        font-size: 15px; | 
|  | 136 | +        font-weight: 600; | 
|  | 137 | +        color: var(--secondary); | 
|  | 138 | +    } | 
|  | 139 | +
 | 
|  | 140 | +    .duration-badge { | 
|  | 141 | +        background: var(--button-elevated); | 
|  | 142 | +        color: var(--button-text); | 
|  | 143 | +        padding: 4px 8px; | 
|  | 144 | +        border-radius: 6px; | 
|  | 145 | +        font-size: 11px; | 
|  | 146 | +        font-weight: 600; | 
|  | 147 | +        font-family: 'IBM Plex Mono', monospace; | 
|  | 148 | +    } | 
|  | 149 | +
 | 
|  | 150 | +    .video-info { | 
|  | 151 | +        display: flex; | 
|  | 152 | +        flex-direction: column; | 
|  | 153 | +        gap: 4px; | 
|  | 154 | +    } | 
|  | 155 | +
 | 
|  | 156 | +    .video-title { | 
|  | 157 | +        font-size: 14px; | 
|  | 158 | +        font-weight: 500; | 
|  | 159 | +        color: var(--secondary); | 
|  | 160 | +        overflow: hidden; | 
|  | 161 | +        text-overflow: ellipsis; | 
|  | 162 | +        white-space: nowrap; | 
|  | 163 | +        max-width: 100%; | 
|  | 164 | +    } | 
|  | 165 | +
 | 
|  | 166 | +    .video-author { | 
|  | 167 | +        font-size: 12px; | 
|  | 168 | +        color: var(--gray); | 
|  | 169 | +        font-weight: 400; | 
|  | 170 | +    } | 
|  | 171 | +
 | 
|  | 172 | +    .clip-time-display { | 
|  | 173 | +        margin-top: 8px; | 
|  | 174 | +    } | 
|  | 175 | +
 | 
|  | 176 | +    .time-indicators { | 
|  | 177 | +        display: flex; | 
|  | 178 | +        justify-content: space-between; | 
|  | 179 | +        align-items: center; | 
|  | 180 | +        font-size: 13px; | 
|  | 181 | +        font-weight: 500; | 
|  | 182 | +        font-family: 'IBM Plex Mono', monospace; | 
|  | 183 | +    } | 
|  | 184 | +
 | 
|  | 185 | +    .start-time, .end-time { | 
|  | 186 | +        color: var(--gray); | 
|  | 187 | +        background: var(--button-hover); | 
|  | 188 | +        padding: 2px 6px; | 
|  | 189 | +        border-radius: 4px; | 
|  | 190 | +        min-width: 50px; | 
|  | 191 | +        text-align: center; | 
|  | 192 | +    } | 
|  | 193 | +
 | 
|  | 194 | +    .duration-selected { | 
|  | 195 | +        grid-area: duration; | 
|  | 196 | +        text-align: center; | 
|  | 197 | +    } | 
|  | 198 | +
 | 
|  | 199 | +    @keyframes slideIn { | 
|  | 200 | +        from { | 
|  | 201 | +            opacity: 0; | 
|  | 202 | +            transform: translateY(-10px); | 
|  | 203 | +        } | 
|  | 204 | +        to { | 
|  | 205 | +            opacity: 1; | 
|  | 206 | +            transform: translateY(0); | 
|  | 207 | +        } | 
|  | 208 | +    } | 
|  | 209 | +
 | 
|  | 210 | +    @keyframes spin { | 
|  | 211 | +        0% { transform: rotate(0deg); } | 
|  | 212 | +        100% { transform: rotate(360deg); } | 
|  | 213 | +    } | 
|  | 214 | +
 | 
|  | 215 | +    @media screen and (max-width: 440px) { | 
|  | 216 | +        .clip-controls { | 
|  | 217 | +            padding: 12px; | 
|  | 218 | +        } | 
|  | 219 | +
 | 
|  | 220 | +        .metadata-header { | 
|  | 221 | +            flex-direction: column; | 
|  | 222 | +            align-items: flex-start; | 
|  | 223 | +            gap: 8px; | 
|  | 224 | +        } | 
|  | 225 | +         | 
|  | 226 | +        .video-title { | 
|  | 227 | +            white-space: normal; | 
|  | 228 | +        } | 
|  | 229 | +
 | 
|  | 230 | +        .time-indicators { | 
|  | 231 | +            display: grid; | 
|  | 232 | +            grid-template-columns: 1fr 1fr; | 
|  | 233 | +            grid-template-areas: | 
|  | 234 | +                "start end" | 
|  | 235 | +                "duration duration"; | 
|  | 236 | +            gap: 8px; | 
|  | 237 | +        } | 
|  | 238 | +
 | 
|  | 239 | +        .start-time { | 
|  | 240 | +            grid-area: start; | 
|  | 241 | +        } | 
|  | 242 | +
 | 
|  | 243 | +        .end-time { | 
|  | 244 | +            grid-area: end; | 
|  | 245 | +        } | 
|  | 246 | +    } | 
|  | 247 | +</style> | 
0 commit comments