Improvements to the video handling (download, gallery view etc..)
This commit is contained in:
parent
1854ecd8e0
commit
499094c25a
13 changed files with 123 additions and 31 deletions
|
|
@ -18,6 +18,7 @@
|
||||||
// View state management for gallery
|
// View state management for gallery
|
||||||
let currentView = $state('main'); // 'main' | 'gallery'
|
let currentView = $state('main'); // 'main' | 'gallery'
|
||||||
let galleryFolder = $state(null);
|
let galleryFolder = $state(null);
|
||||||
|
let savedScrollPosition = $state(0);
|
||||||
|
|
||||||
// Output folder management
|
// Output folder management
|
||||||
let outputFolderRefreshKey = $state(0);
|
let outputFolderRefreshKey = $state(0);
|
||||||
|
|
@ -27,13 +28,48 @@
|
||||||
jobStatus === 'running' || jobStatus === 'compiling_video' || jobStatus === 'cancelling'
|
jobStatus === 'running' || jobStatus === 'compiling_video' || jobStatus === 'cancelling'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Match backend's sanitize_folder_name logic for video path construction
|
||||||
|
function sanitizeFolderName(name, id) {
|
||||||
|
const base = name && name.trim() ? name : id;
|
||||||
|
let sanitized = '';
|
||||||
|
for (const c of base) {
|
||||||
|
if (/^[\p{L}\p{N}\-_ ]$/u.test(c)) {
|
||||||
|
sanitized += c;
|
||||||
|
} else {
|
||||||
|
sanitized += '_';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sanitized = sanitized.trim();
|
||||||
|
return sanitized.length > 50 ? sanitized.slice(0, 50) : sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute folder name from progress for video display
|
||||||
|
let completedFolderName = $derived(
|
||||||
|
progress.person_name || progress.person_id
|
||||||
|
? sanitizeFolderName(progress.person_name, progress.person_id)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
function handleFoldersLoaded(folders) {
|
function handleFoldersLoaded(folders) {
|
||||||
outputFolders = folders;
|
outputFolders = folders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleFolderDeleted(folderName) {
|
||||||
|
// If the deleted folder matches the completed job's folder, reset to idle
|
||||||
|
// folderName === null means all folders were deleted
|
||||||
|
if (jobStatus === 'completed' && (folderName === null || folderName === completedFolderName)) {
|
||||||
|
jobStatus = 'idle';
|
||||||
|
progress = { completed: 0, total: 0, message: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openGallery(folder) {
|
function openGallery(folder) {
|
||||||
|
// Save scroll position before switching to gallery
|
||||||
|
savedScrollPosition = window.scrollY;
|
||||||
galleryFolder = folder;
|
galleryFolder = folder;
|
||||||
currentView = 'gallery';
|
currentView = 'gallery';
|
||||||
|
// Scroll to top for gallery view
|
||||||
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeGallery() {
|
function closeGallery() {
|
||||||
|
|
@ -41,6 +77,10 @@
|
||||||
currentView = 'main';
|
currentView = 'main';
|
||||||
// Check for any running job (e.g., video compilation started from gallery)
|
// Check for any running job (e.g., video compilation started from gallery)
|
||||||
checkAndPollProgress();
|
checkAndPollProgress();
|
||||||
|
// Restore scroll position after DOM updates
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.scrollTo(0, savedScrollPosition);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConnectionChange(data) {
|
function handleConnectionChange(data) {
|
||||||
|
|
@ -70,11 +110,16 @@
|
||||||
const res = await fetch('/api/progress');
|
const res = await fetch('/api/progress');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
jobStatus = data.status;
|
// Only restore status if a job is actively running, or if we're not in idle state.
|
||||||
progress = data;
|
// This prevents restoring 'completed' status after we've manually dismissed it.
|
||||||
|
const isActiveJob = data.status === 'running' || data.status === 'compiling_video' || data.status === 'cancelling';
|
||||||
|
if (isActiveJob || jobStatus !== 'idle') {
|
||||||
|
jobStatus = data.status;
|
||||||
|
progress = data;
|
||||||
|
}
|
||||||
|
|
||||||
// If a job is running, start polling
|
// If a job is running, start polling
|
||||||
if (data.status === 'running' || data.status === 'compiling_video' || data.status === 'cancelling') {
|
if (isActiveJob) {
|
||||||
startPolling();
|
startPolling();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -170,7 +215,7 @@
|
||||||
|
|
||||||
{#if jobStatus === 'completed'}
|
{#if jobStatus === 'completed'}
|
||||||
<section class="results">
|
<section class="results">
|
||||||
<ResultsView />
|
<ResultsView folderName={completedFolderName} />
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
@ -180,6 +225,7 @@
|
||||||
onOpenGallery={openGallery}
|
onOpenGallery={openGallery}
|
||||||
refreshKey={outputFolderRefreshKey}
|
refreshKey={outputFolderRefreshKey}
|
||||||
onFoldersLoaded={handleFoldersLoaded}
|
onFoldersLoaded={handleFoldersLoaded}
|
||||||
|
onFolderDeleted={handleFolderDeleted}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
<span>Immich {version}</span>
|
<span>Immich {version}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="indicator error"></span>
|
<span class="indicator error"></span>
|
||||||
<button onclick={checkConnection} class="retry">
|
<button type="button" onclick={checkConnection} class="retry">
|
||||||
{error} - Retry
|
{error} - Retry
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@
|
||||||
|
|
||||||
<div class="gallery-view">
|
<div class="gallery-view">
|
||||||
<header class="gallery-header">
|
<header class="gallery-header">
|
||||||
<button class="back-btn" onclick={onBack} disabled={disabled || deleting || compiling}>
|
<button type="button" class="back-btn" onclick={onBack} disabled={disabled || deleting || compiling}>
|
||||||
← Back
|
← Back
|
||||||
</button>
|
</button>
|
||||||
<h2>
|
<h2>
|
||||||
|
|
@ -152,6 +152,7 @@
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="selection-controls">
|
<div class="selection-controls">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="toolbar-btn"
|
class="toolbar-btn"
|
||||||
onclick={selectAll}
|
onclick={selectAll}
|
||||||
disabled={disabled || deleting || compiling || allSelected}
|
disabled={disabled || deleting || compiling || allSelected}
|
||||||
|
|
@ -159,6 +160,7 @@
|
||||||
Select All
|
Select All
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="toolbar-btn"
|
class="toolbar-btn"
|
||||||
onclick={deselectAll}
|
onclick={deselectAll}
|
||||||
disabled={disabled || deleting || compiling || selectedCount === 0}
|
disabled={disabled || deleting || compiling || selectedCount === 0}
|
||||||
|
|
@ -166,6 +168,7 @@
|
||||||
Deselect All
|
Deselect All
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="toolbar-btn delete-btn"
|
class="toolbar-btn delete-btn"
|
||||||
onclick={deleteSelected}
|
onclick={deleteSelected}
|
||||||
disabled={disabled || deleting || compiling || selectedCount === 0}
|
disabled={disabled || deleting || compiling || selectedCount === 0}
|
||||||
|
|
@ -213,6 +216,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="compile-btn"
|
class="compile-btn"
|
||||||
onclick={compileVideo}
|
onclick={compileVideo}
|
||||||
disabled={disabled || deleting || compiling || images.length === 0}
|
disabled={disabled || deleting || compiling || images.length === 0}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let { disabled = false, onOpenGallery, refreshKey = 0, onFoldersLoaded } = $props();
|
let { disabled = false, onOpenGallery, refreshKey = 0, onFoldersLoaded, onFolderDeleted } = $props();
|
||||||
|
|
||||||
let folders = $state([]);
|
let folders = $state([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
|
@ -45,6 +45,8 @@
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
throw new Error(data.message || 'Failed to delete folder');
|
throw new Error(data.message || 'Failed to delete folder');
|
||||||
}
|
}
|
||||||
|
// Notify parent that folder was deleted
|
||||||
|
onFolderDeleted?.(name);
|
||||||
// Reload the list
|
// Reload the list
|
||||||
await loadFolders();
|
await loadFolders();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -68,6 +70,8 @@
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
throw new Error(data.message || 'Failed to delete folders');
|
throw new Error(data.message || 'Failed to delete folders');
|
||||||
}
|
}
|
||||||
|
// Notify parent that all folders were deleted (null = all)
|
||||||
|
onFolderDeleted?.(null);
|
||||||
// Reload the list
|
// Reload the list
|
||||||
await loadFolders();
|
await loadFolders();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -91,7 +95,7 @@
|
||||||
<div class="output-manager">
|
<div class="output-manager">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h3>Output Folders</h3>
|
<h3>Output Folders</h3>
|
||||||
<button class="refresh-btn" onclick={loadFolders} disabled={loading || disabled}>
|
<button type="button" class="refresh-btn" onclick={loadFolders} disabled={loading || disabled}>
|
||||||
{loading ? '...' : '↻'}
|
{loading ? '...' : '↻'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -118,14 +122,22 @@
|
||||||
<div class="folder-actions">
|
<div class="folder-actions">
|
||||||
{#if folder.has_video}
|
{#if folder.has_video}
|
||||||
<a
|
<a
|
||||||
href="/output/{encodeURIComponent(folder.name)}/timelapse.mp4"
|
href="/output/{encodeURIComponent(folder.name)}/{encodeURIComponent(folder.name)}.mp4"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="view-btn"
|
class="action-btn"
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
href="/output/{encodeURIComponent(folder.name)}/{encodeURIComponent(folder.name)}.mp4"
|
||||||
|
download="{folder.name}.mp4"
|
||||||
|
class="action-btn"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="gallery-btn"
|
class="gallery-btn"
|
||||||
onclick={() => onOpenGallery?.(folder)}
|
onclick={() => onOpenGallery?.(folder)}
|
||||||
disabled={disabled || deleting !== null}
|
disabled={disabled || deleting !== null}
|
||||||
|
|
@ -133,6 +145,7 @@
|
||||||
Gallery
|
Gallery
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="delete-btn"
|
class="delete-btn"
|
||||||
onclick={() => deleteFolder(folder.name)}
|
onclick={() => deleteFolder(folder.name)}
|
||||||
disabled={disabled || deleting !== null}
|
disabled={disabled || deleting !== null}
|
||||||
|
|
@ -146,6 +159,7 @@
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="delete-all-btn"
|
class="delete-all-btn"
|
||||||
onclick={deleteAll}
|
onclick={deleteAll}
|
||||||
disabled={disabled || deleting !== null || folders.length === 0}
|
disabled={disabled || deleting !== null || folders.length === 0}
|
||||||
|
|
@ -273,7 +287,7 @@
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-btn {
|
.action-btn {
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
background: #333;
|
background: #333;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -285,7 +299,7 @@
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-btn:hover {
|
.action-btn:hover {
|
||||||
background: #4f46e5;
|
background: #4f46e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<div class="error">
|
<div class="error">
|
||||||
{error}
|
{error}
|
||||||
<button onclick={loadPeople}>Retry</button>
|
<button type="button" onclick={loadPeople}>Retry</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<input
|
<input
|
||||||
|
|
@ -64,6 +64,7 @@
|
||||||
<div class="people-grid">
|
<div class="people-grid">
|
||||||
{#each filteredPeople as person (person.id)}
|
{#each filteredPeople as person (person.id)}
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="person-card"
|
class="person-card"
|
||||||
class:selected={selectedId === person.id}
|
class:selected={selectedId === person.id}
|
||||||
onclick={() => selectPerson(person)}
|
onclick={() => selectPerson(person)}
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="start-btn" onclick={handleStartClick}>
|
<button type="button" class="start-btn" onclick={handleStartClick}>
|
||||||
Start Processing
|
Start Processing
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@
|
||||||
<span class="count">{progress.completed} / {progress.total}</span>
|
<span class="count">{progress.completed} / {progress.total}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if canCancel}
|
{#if canCancel}
|
||||||
<button class="cancel-btn" onclick={cancelProcessing}>Cancel</button>
|
<button type="button" class="cancel-btn" onclick={cancelProcessing}>Cancel</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,13 @@
|
||||||
// - Skipped images with reasons
|
// - Skipped images with reasons
|
||||||
// - Face landmark visualization
|
// - Face landmark visualization
|
||||||
|
|
||||||
let videoUrl = $state('/output/timelapse.mp4');
|
let { folderName = null } = $props();
|
||||||
|
|
||||||
|
let videoUrl = $derived(
|
||||||
|
folderName
|
||||||
|
? `/output/${encodeURIComponent(folderName)}/${encodeURIComponent(folderName)}.mp4`
|
||||||
|
: null
|
||||||
|
);
|
||||||
let videoError = $state(false);
|
let videoError = $state(false);
|
||||||
|
|
||||||
function handleVideoError() {
|
function handleVideoError() {
|
||||||
|
|
@ -16,7 +22,12 @@
|
||||||
<div class="results-view">
|
<div class="results-view">
|
||||||
<h2>Result</h2>
|
<h2>Result</h2>
|
||||||
|
|
||||||
{#if videoError}
|
{#if !videoUrl}
|
||||||
|
<div class="video-error">
|
||||||
|
<p>No video available.</p>
|
||||||
|
<p class="hint">Person information was not provided.</p>
|
||||||
|
</div>
|
||||||
|
{:else if videoError}
|
||||||
<div class="video-error">
|
<div class="video-error">
|
||||||
<p>Video not available yet.</p>
|
<p>Video not available yet.</p>
|
||||||
<p class="hint">The video file may still be processing or the path may have changed.</p>
|
<p class="hint">The video file may still be processing or the path may have changed.</p>
|
||||||
|
|
@ -36,7 +47,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a href={videoUrl} download="timelapse.mp4" class="download-btn">
|
<a href={videoUrl} download="{folderName}.mp4" class="download-btn">
|
||||||
Download Video
|
Download Video
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="settings-panel">
|
<div class="settings-panel">
|
||||||
<button class="toggle-btn" onclick={toggle}>
|
<button type="button" class="toggle-btn" onclick={toggle}>
|
||||||
<span class="icon">{isOpen ? '▼' : '▶'}</span>
|
<span class="icon">{isOpen ? '▼' : '▶'}</span>
|
||||||
Settings
|
Settings
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -90,11 +90,12 @@
|
||||||
<p class="loading">Loading settings...</p>
|
<p class="loading">Loading settings...</p>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<p class="error">{error}</p>
|
<p class="error">{error}</p>
|
||||||
<button class="retry-btn" onclick={loadConfig}>Retry</button>
|
<button type="button" class="retry-btn" onclick={loadConfig}>Retry</button>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Tab navigation -->
|
<!-- Tab navigation -->
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="tab"
|
class="tab"
|
||||||
class:active={activeTab === 'face'}
|
class:active={activeTab === 'face'}
|
||||||
onclick={() => (activeTab = 'face')}
|
onclick={() => (activeTab = 'face')}
|
||||||
|
|
@ -102,6 +103,7 @@
|
||||||
Face
|
Face
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="tab"
|
class="tab"
|
||||||
class:active={activeTab === 'output'}
|
class:active={activeTab === 'output'}
|
||||||
onclick={() => (activeTab = 'output')}
|
onclick={() => (activeTab = 'output')}
|
||||||
|
|
@ -109,6 +111,7 @@
|
||||||
Output
|
Output
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="tab"
|
class="tab"
|
||||||
class:active={activeTab === 'video'}
|
class:active={activeTab === 'video'}
|
||||||
onclick={() => (activeTab = 'video')}
|
onclick={() => (activeTab = 'video')}
|
||||||
|
|
@ -276,7 +279,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="save-btn" onclick={saveConfig} disabled={disabled || saving}>
|
<button type="button" class="save-btn" onclick={saveConfig} disabled={disabled || saving}>
|
||||||
{saving ? 'Saving...' : 'Save Settings'}
|
{saving ? 'Saving...' : 'Save Settings'}
|
||||||
</button>
|
</button>
|
||||||
{#if saveMessage}
|
{#if saveMessage}
|
||||||
|
|
|
||||||
|
|
@ -264,7 +264,6 @@ impl ImmichClient {
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let bytes = response.bytes().await?;
|
let bytes = response.bytes().await?;
|
||||||
tracing::debug!("Thumbnail bytes received: {}", bytes.len());
|
|
||||||
Ok((bytes, content_type))
|
Ok((bytes, content_type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -180,9 +180,10 @@ async fn run_job_inner(
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let video_filename = format!("{}.mp4", folder_name);
|
||||||
let output_dirs = OutputDirs {
|
let output_dirs = OutputDirs {
|
||||||
images: images_dir.clone(),
|
images: images_dir.clone(),
|
||||||
video: person_dir.join("timelapse.mp4"),
|
video: person_dir.join(&video_filename),
|
||||||
debug,
|
debug,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -339,9 +339,9 @@ async fn start_processing(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset progress with person info
|
// Reset progress with person info (bypasses terminal state check)
|
||||||
state
|
state
|
||||||
.update_progress(Progress {
|
.reset_progress(Progress {
|
||||||
status: JobStatus::Running,
|
status: JobStatus::Running,
|
||||||
completed: 0,
|
completed: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
|
|
@ -485,8 +485,9 @@ async fn list_output_folders(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for video file
|
// Check for video file (named after the folder/person)
|
||||||
let has_video = path.join("timelapse.mp4").exists();
|
let video_filename = format!("{}.mp4", name);
|
||||||
|
let has_video = path.join(&video_filename).exists();
|
||||||
|
|
||||||
folders.push(OutputFolderInfo {
|
folders.push(OutputFolderInfo {
|
||||||
name,
|
name,
|
||||||
|
|
@ -678,10 +679,11 @@ async fn list_folder_images(
|
||||||
images.sort_by(|a, b| a.filename.cmp(&b.filename));
|
images.sort_by(|a, b| a.filename.cmp(&b.filename));
|
||||||
|
|
||||||
let total_count = images.len() as u32;
|
let total_count = images.len() as u32;
|
||||||
|
let video_filename = format!("{}.mp4", folder_name);
|
||||||
let video_exists = config
|
let video_exists = config
|
||||||
.output_dir
|
.output_dir
|
||||||
.join(&folder_name)
|
.join(&folder_name)
|
||||||
.join("timelapse.mp4")
|
.join(&video_filename)
|
||||||
.exists();
|
.exists();
|
||||||
|
|
||||||
Ok(Json(FolderImagesResponse {
|
Ok(Json(FolderImagesResponse {
|
||||||
|
|
@ -890,9 +892,9 @@ async fn compile_folder_video(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set status to compiling video
|
// Set status to compiling video (bypasses terminal state check)
|
||||||
state
|
state
|
||||||
.update_progress(Progress {
|
.reset_progress(Progress {
|
||||||
status: JobStatus::CompilingVideo,
|
status: JobStatus::CompilingVideo,
|
||||||
completed: 0,
|
completed: 0,
|
||||||
total: image_count,
|
total: image_count,
|
||||||
|
|
@ -908,7 +910,8 @@ async fn compile_folder_video(
|
||||||
|
|
||||||
// Clone values needed for the async task
|
// Clone values needed for the async task
|
||||||
let video_config = config.video.clone();
|
let video_config = config.video.clone();
|
||||||
let output_path = folder_path.join("timelapse.mp4");
|
let video_filename = format!("{}.mp4", folder_name);
|
||||||
|
let output_path = folder_path.join(&video_filename);
|
||||||
let job_state = state.clone();
|
let job_state = state.clone();
|
||||||
let folder_name_clone = folder_name.clone();
|
let folder_name_clone = folder_name.clone();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,16 @@ impl AppState {
|
||||||
let _ = self.progress_tx.send(progress);
|
let _ = self.progress_tx.send(progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reset progress for a new job, bypassing the terminal state check.
|
||||||
|
///
|
||||||
|
/// Use this when starting a new job to clear any previous terminal state
|
||||||
|
/// (Completed, Cancelled, Error) that would otherwise block progress updates.
|
||||||
|
pub async fn reset_progress(&self, progress: Progress) {
|
||||||
|
let mut current = self.progress.write().await;
|
||||||
|
*current = progress.clone();
|
||||||
|
let _ = self.progress_tx.send(progress);
|
||||||
|
}
|
||||||
|
|
||||||
/// Request cancellation of the current job.
|
/// Request cancellation of the current job.
|
||||||
pub async fn request_cancel(&self) -> bool {
|
pub async fn request_cancel(&self) -> bool {
|
||||||
let cancel_token = self.cancel_token.read().await;
|
let cancel_token = self.cancel_token.read().await;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue