mirror of
https://github.com/imputnet/cobalt.git
synced 2025-07-10 15:28:29 +00:00
app: clean up and update request types
This commit is contained in:
parent
2a1aaef0b0
commit
1eb296cd8b
@ -32,7 +32,6 @@
|
|||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-rate-limit": "^7.4.1",
|
"express-rate-limit": "^7.4.1",
|
||||||
"ffmpeg-static": "^5.1.0",
|
"ffmpeg-static": "^5.1.0",
|
||||||
"ffprobe-static": "^3.1.0",
|
|
||||||
"hls-parser": "^0.10.7",
|
"hls-parser": "^0.10.7",
|
||||||
"ipaddr.js": "2.2.0",
|
"ipaddr.js": "2.2.0",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
|
@ -330,11 +330,17 @@ export default async function({ host, patternMatch, params, authType }) {
|
|||||||
const lpEnv = env.forceLocalProcessing;
|
const lpEnv = env.forceLocalProcessing;
|
||||||
const shouldForceLocal = lpEnv === "always" || (lpEnv === "session" && authType === "session");
|
const shouldForceLocal = lpEnv === "always" || (lpEnv === "session" && authType === "session");
|
||||||
const localDisabled = (!localProcessing || localProcessing === "none");
|
const localDisabled = (!localProcessing || localProcessing === "none");
|
||||||
|
const isClip = typeof params.clipStart === 'number' && typeof params.clipEnd === 'number';
|
||||||
|
|
||||||
if (shouldForceLocal && localDisabled) {
|
if (shouldForceLocal && localDisabled) {
|
||||||
localProcessing = "preferred";
|
localProcessing = "preferred";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isClip) {
|
||||||
|
r.clipStart = params.clipStart;
|
||||||
|
r.clipEnd = params.clipEnd;
|
||||||
|
}
|
||||||
|
|
||||||
return matchAction({
|
return matchAction({
|
||||||
r,
|
r,
|
||||||
host,
|
host,
|
||||||
|
@ -291,13 +291,6 @@ export default async function (o) {
|
|||||||
return { error: "content.too_long" };
|
return { error: "content.too_long" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof o.clipStart === 'number' && o.clipStart >= basicInfo.duration) {
|
|
||||||
return { error: "clip.start_exceeds_duration" };
|
|
||||||
}
|
|
||||||
if (typeof o.clipEnd === 'number' && o.clipEnd > basicInfo.duration) {
|
|
||||||
return { error: "clip.end_exceeds_duration" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// return a critical error if returned video is "Video Not Available"
|
// return a critical error if returned video is "Video Not Available"
|
||||||
// or a similar stub by youtube
|
// or a similar stub by youtube
|
||||||
if (basicInfo.id !== o.id) {
|
if (basicInfo.id !== o.id) {
|
||||||
@ -487,7 +480,7 @@ export default async function (o) {
|
|||||||
|
|
||||||
const fileMetadata = {
|
const fileMetadata = {
|
||||||
title: basicInfo.title.trim(),
|
title: basicInfo.title.trim(),
|
||||||
artist: basicInfo.author.replace("- Topic", "").trim(),
|
artist: basicInfo.author.replace("- Topic", "").trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (basicInfo?.short_description?.startsWith("Provided to YouTube by")) {
|
if (basicInfo?.short_description?.startsWith("Provided to YouTube by")) {
|
||||||
@ -581,8 +574,6 @@ export default async function (o) {
|
|||||||
|
|
||||||
cover,
|
cover,
|
||||||
cropCover: basicInfo.author.endsWith("- Topic"),
|
cropCover: basicInfo.author.endsWith("- Topic"),
|
||||||
clipStart: o.clipStart,
|
|
||||||
clipEnd: o.clipEnd,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -628,9 +619,7 @@ export default async function (o) {
|
|||||||
fileMetadata,
|
fileMetadata,
|
||||||
isHLS: useHLS,
|
isHLS: useHLS,
|
||||||
originalRequest,
|
originalRequest,
|
||||||
duration: basicInfo.duration,
|
duration: basicInfo.duration
|
||||||
clipStart: o.clipStart,
|
|
||||||
clipEnd: o.clipEnd,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,9 +100,7 @@ const remux = async (streamInfo, res) => {
|
|||||||
const format = streamInfo.filename.split('.').pop();
|
const format = streamInfo.filename.split('.').pop();
|
||||||
const urls = Array.isArray(streamInfo.urls) ? streamInfo.urls : [streamInfo.urls];
|
const urls = Array.isArray(streamInfo.urls) ? streamInfo.urls : [streamInfo.urls];
|
||||||
const isClipping = typeof streamInfo.clipStart === 'number' || typeof streamInfo.clipEnd === 'number';
|
const isClipping = typeof streamInfo.clipStart === 'number' || typeof streamInfo.clipEnd === 'number';
|
||||||
let args = [];
|
const args = urls.flatMap(url => ['-i', url]);
|
||||||
|
|
||||||
args.push(...urls.flatMap(url => ['-i', url]));
|
|
||||||
|
|
||||||
if (typeof streamInfo.clipStart === 'number') {
|
if (typeof streamInfo.clipStart === 'number') {
|
||||||
args.push('-ss', streamInfo.clipStart.toString());
|
args.push('-ss', streamInfo.clipStart.toString());
|
||||||
@ -111,6 +109,7 @@ const remux = async (streamInfo, res) => {
|
|||||||
args.push('-to', streamInfo.clipEnd.toString());
|
args.push('-to', streamInfo.clipEnd.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the stream type is merge, we expect two URLs
|
||||||
if (streamInfo.type === 'merge' && urls.length !== 2) {
|
if (streamInfo.type === 'merge' && urls.length !== 2) {
|
||||||
return closeResponse(res);
|
return closeResponse(res);
|
||||||
}
|
}
|
||||||
@ -171,13 +170,11 @@ const remux = async (streamInfo, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const convertAudio = async (streamInfo, res) => {
|
const convertAudio = async (streamInfo, res) => {
|
||||||
let args = [];
|
const args = [
|
||||||
|
|
||||||
args.push(
|
|
||||||
'-i', streamInfo.urls,
|
'-i', streamInfo.urls,
|
||||||
'-vn',
|
'-vn',
|
||||||
...(streamInfo.audioCopy ? ['-c:a', 'copy'] : ['-b:a', `${streamInfo.audioBitrate}k`]),
|
...(streamInfo.audioCopy ? ['-c:a', 'copy'] : ['-b:a', `${streamInfo.audioBitrate}k`]),
|
||||||
);
|
];
|
||||||
|
|
||||||
if (typeof streamInfo.clipStart === 'number') {
|
if (typeof streamInfo.clipStart === 'number') {
|
||||||
args.push('-ss', streamInfo.clipStart.toString());
|
args.push('-ss', streamInfo.clipStart.toString());
|
||||||
|
@ -37,9 +37,6 @@ importers:
|
|||||||
ffmpeg-static:
|
ffmpeg-static:
|
||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.2.0
|
version: 5.2.0
|
||||||
ffprobe-static:
|
|
||||||
specifier: ^3.1.0
|
|
||||||
version: 3.1.0
|
|
||||||
hls-parser:
|
hls-parser:
|
||||||
specifier: ^0.10.7
|
specifier: ^0.10.7
|
||||||
version: 0.10.9
|
version: 0.10.9
|
||||||
@ -1257,9 +1254,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==}
|
resolution: {integrity: sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
ffprobe-static@3.1.0:
|
|
||||||
resolution: {integrity: sha512-Dvpa9uhVMOYivhHKWLGDoa512J751qN1WZAIO+Xw4L/mrUSPxS4DApzSUDbCFE/LUq2+xYnznEahTd63AqBSpA==}
|
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
@ -3195,8 +3189,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
ffprobe-static@3.1.0: {}
|
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 4.0.1
|
flat-cache: 4.0.1
|
||||||
|
247
web/src/components/save/ClipControls.svelte
Normal file
247
web/src/components/save/ClipControls.svelte
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ClipRangeSlider from "./ClipRangeSlider.svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
metaLoading,
|
||||||
|
metaError,
|
||||||
|
metadata,
|
||||||
|
clipStart = $bindable(),
|
||||||
|
clipEnd = $bindable()
|
||||||
|
} = $props<{
|
||||||
|
metaLoading: boolean;
|
||||||
|
metaError: string | null;
|
||||||
|
metadata: { title?: string; author?: string; duration?: number } | null;
|
||||||
|
clipStart: number;
|
||||||
|
clipEnd: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function formatTime(seconds: number) {
|
||||||
|
const m = Math.floor(seconds / 60);
|
||||||
|
const s = (seconds % 60).toFixed(3).padStart(6, '0');
|
||||||
|
return `${m}:${s}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="clip-controls">
|
||||||
|
{#if metaLoading}
|
||||||
|
<div class="loading-state">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<span>Loading video metadata...</span>
|
||||||
|
</div>
|
||||||
|
{:else if metaError}
|
||||||
|
<div class="error-state">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||||
|
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||||
|
</svg>
|
||||||
|
<span>{metaError}</span>
|
||||||
|
</div>
|
||||||
|
{:else if metadata}
|
||||||
|
<div class="clip-metadata">
|
||||||
|
<div class="metadata-header">
|
||||||
|
<h4>Clip</h4>
|
||||||
|
<div class="duration-badge">
|
||||||
|
{typeof metadata.duration === 'number' ? formatTime(metadata.duration) : 'Unknown'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="video-info">
|
||||||
|
<div class="video-title" title={metadata.title}>
|
||||||
|
{metadata.title}
|
||||||
|
</div>
|
||||||
|
{#if metadata.author}
|
||||||
|
<div class="video-author">
|
||||||
|
by {metadata.author}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ClipRangeSlider
|
||||||
|
min={0}
|
||||||
|
max={metadata.duration || 0}
|
||||||
|
step={0.001}
|
||||||
|
bind:start={clipStart}
|
||||||
|
bind:end={clipEnd}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="clip-time-display">
|
||||||
|
<div class="time-indicators">
|
||||||
|
<span class="start-time">{formatTime(clipStart)}</span>
|
||||||
|
<span class="duration-selected">
|
||||||
|
{formatTime(Math.max(0, clipEnd - clipStart))} selected
|
||||||
|
</span>
|
||||||
|
<span class="end-time">{formatTime(clipEnd)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.clip-controls {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--button);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 1px solid var(--button-stroke);
|
||||||
|
animation: slideIn 0.3s cubic-bezier(0.2, 0, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 0;
|
||||||
|
color: var(--gray);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid var(--button-hover);
|
||||||
|
border-top: 2px solid var(--secondary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(237, 34, 54, 0.1);
|
||||||
|
border: 1px solid rgba(237, 34, 54, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--red);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clip-metadata {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-badge {
|
||||||
|
background: var(--button-elevated);
|
||||||
|
color: var(--button-text);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-author {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gray);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clip-time-display {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-indicators {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-time, .end-time {
|
||||||
|
color: var(--gray);
|
||||||
|
background: var(--button-hover);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-selected {
|
||||||
|
grid-area: duration;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 440px) {
|
||||||
|
.clip-controls {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-title {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-indicators {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"start end"
|
||||||
|
"duration duration";
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-time {
|
||||||
|
grid-area: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-time {
|
||||||
|
grid-area: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -37,6 +37,7 @@
|
|||||||
import API from "$lib/api/api";
|
import API from "$lib/api/api";
|
||||||
import ClipRangeSlider from "./ClipRangeSlider.svelte";
|
import ClipRangeSlider from "./ClipRangeSlider.svelte";
|
||||||
import ClipCheckbox from "./buttons/ClipCheckbox.svelte";
|
import ClipCheckbox from "./buttons/ClipCheckbox.svelte";
|
||||||
|
import ClipControls from "./ClipControls.svelte";
|
||||||
|
|
||||||
let linkInput: Optional<HTMLInputElement>;
|
let linkInput: Optional<HTMLInputElement>;
|
||||||
|
|
||||||
@ -284,60 +285,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if clipMode && validLink($link)}
|
{#if clipMode && validLink($link)}
|
||||||
<div class="clip-controls">
|
<ClipControls
|
||||||
{#if metaLoading}
|
{metaLoading}
|
||||||
<div class="loading-state">
|
{metaError}
|
||||||
<div class="loading-spinner"></div>
|
{metadata}
|
||||||
<span>Loading video metadata...</span>
|
bind:clipStart
|
||||||
</div>
|
bind:clipEnd
|
||||||
{:else if metaError}
|
|
||||||
<div class="error-state">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
|
||||||
<line x1="15" y1="9" x2="9" y2="15"></line>
|
|
||||||
<line x1="9" y1="9" x2="15" y2="15"></line>
|
|
||||||
</svg>
|
|
||||||
<span>{metaError}</span>
|
|
||||||
</div>
|
|
||||||
{:else if metadata}
|
|
||||||
<div class="clip-metadata">
|
|
||||||
<div class="metadata-header">
|
|
||||||
<h4>Clip</h4>
|
|
||||||
<div class="duration-badge">
|
|
||||||
{typeof metadata.duration === 'number' ? formatTime(metadata.duration) : 'Unknown'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="video-info">
|
|
||||||
<div class="video-title" title={metadata.title}>
|
|
||||||
{metadata.title}
|
|
||||||
</div>
|
|
||||||
{#if metadata.author}
|
|
||||||
<div class="video-author">
|
|
||||||
by {metadata.author}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ClipRangeSlider
|
|
||||||
min={0}
|
|
||||||
max={metadata.duration || 0}
|
|
||||||
step={0.001}
|
|
||||||
bind:start={clipStart}
|
|
||||||
bind:end={clipEnd}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="clip-time-display">
|
|
||||||
<div class="time-indicators">
|
|
||||||
<span class="start-time">{formatTime(clipStart)}</span>
|
|
||||||
<span class="duration-selected">
|
|
||||||
{formatTime(Math.max(0, clipEnd - clipStart))} selected
|
|
||||||
</span>
|
|
||||||
<span class="end-time">{formatTime(clipEnd)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -467,140 +421,6 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clip-controls {
|
|
||||||
margin-top: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
background: var(--button);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
border: 1px solid var(--button-stroke);
|
|
||||||
animation: slideIn 0.3s cubic-bezier(0.2, 0, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-state {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px 0;
|
|
||||||
color: var(--gray);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 2px solid var(--button-hover);
|
|
||||||
border-top: 2px solid var(--secondary);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-state {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: rgba(237, 34, 54, 0.1);
|
|
||||||
border: 1px solid rgba(237, 34, 54, 0.2);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: var(--red);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clip-metadata {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata-header h4 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.duration-badge {
|
|
||||||
background: var(--button-elevated);
|
|
||||||
color: var(--button-text);
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--secondary);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-author {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--gray);
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clip-time-display {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-indicators {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-time, .end-time {
|
|
||||||
color: var(--gray);
|
|
||||||
background: var(--button-hover);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
min-width: 50px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.duration-selected {
|
|
||||||
grid-area: duration;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 440px) {
|
@media screen and (max-width: 440px) {
|
||||||
#action-container {
|
#action-container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -618,36 +438,5 @@
|
|||||||
#paste-desktop-text {
|
#paste-desktop-text {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clip-controls {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-title {
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-indicators {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
grid-template-areas:
|
|
||||||
"start end"
|
|
||||||
"duration duration";
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-time {
|
|
||||||
grid-area: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.end-time {
|
|
||||||
grid-area: end;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -61,12 +61,11 @@
|
|||||||
on:click={() => {
|
on:click={() => {
|
||||||
hapticSwitch();
|
hapticSwitch();
|
||||||
if (clipMode) {
|
if (clipMode) {
|
||||||
const req: CobaltSaveRequestBody & { clipStart?: number; clipEnd?: number } = {
|
savingHandler({ request: {
|
||||||
url,
|
url,
|
||||||
clipStart,
|
clipStart,
|
||||||
clipEnd,
|
clipEnd,
|
||||||
};
|
} });
|
||||||
savingHandler({ request: req });
|
|
||||||
} else {
|
} else {
|
||||||
savingHandler({ url });
|
savingHandler({ url });
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,10 @@ export type CobaltServerInfo = {
|
|||||||
// this allows for extra properties, which is not ideal,
|
// this allows for extra properties, which is not ideal,
|
||||||
// but i couldn't figure out how to make a strict partial :(
|
// but i couldn't figure out how to make a strict partial :(
|
||||||
export type CobaltSaveRequestBody =
|
export type CobaltSaveRequestBody =
|
||||||
{ url: string } & Partial<Omit<CobaltSettings['save'], 'savingMethod'>>;
|
{ url: string } & Partial<Omit<CobaltSettings['save'], 'savingMethod'>> & {
|
||||||
|
clipStart?: number,
|
||||||
|
clipEnd?: number,
|
||||||
|
};
|
||||||
|
|
||||||
export type CobaltSessionResponse = CobaltSession | CobaltErrorResponse;
|
export type CobaltSessionResponse = CobaltSession | CobaltErrorResponse;
|
||||||
export type CobaltServerInfoResponse = CobaltServerInfo | CobaltErrorResponse;
|
export type CobaltServerInfoResponse = CobaltServerInfo | CobaltErrorResponse;
|
||||||
|
Loading…
Reference in New Issue
Block a user