mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat(web/server): Add options to rerun job on all assets (#1422)
This commit is contained in:
parent
6ea91b2dde
commit
788b435f9b
17 changed files with 234 additions and 185 deletions
|
|
@ -1,76 +1,102 @@
|
|||
<script lang="ts">
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import SelectionSearch from 'svelte-material-icons/SelectionSearch.svelte';
|
||||
import Play from 'svelte-material-icons/Play.svelte';
|
||||
import AllInclusive from 'svelte-material-icons/AllInclusive.svelte';
|
||||
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { JobCounts } from '@api';
|
||||
|
||||
export let title: string;
|
||||
export let subtitle: string;
|
||||
export let buttonTitle = 'Run';
|
||||
export let jobCounts: JobCounts;
|
||||
/**
|
||||
* Show options to run job on all assets of just missing ones
|
||||
*/
|
||||
export let showOptions = true;
|
||||
|
||||
$: isRunning = jobCounts.active > 0 || jobCounts.waiting > 0;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const run = (includeAllAssets: boolean) => {
|
||||
dispatch('click', { includeAllAssets });
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex border-b pb-5 dark:border-b-immich-dark-gray">
|
||||
<div class="w-[70%]">
|
||||
<h1 class="text-immich-primary dark:text-immich-dark-primary text-sm font-semibold">
|
||||
{title.toUpperCase()}
|
||||
</h1>
|
||||
<p class="text-sm mt-1 dark:text-immich-dark-fg">{subtitle}</p>
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
<slot />
|
||||
</p>
|
||||
<table class="text-left w-full mt-5">
|
||||
<!-- table header -->
|
||||
<thead
|
||||
class="border rounded-md mb-2 dark:bg-immich-dark-gray dark:border-immich-dark-gray bg-immich-primary/10 flex text-immich-primary dark:text-immich-dark-primary w-full h-12"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="text-center w-1/3 font-medium text-sm">Status</th>
|
||||
<th class="text-center w-1/3 font-medium text-sm">Active</th>
|
||||
<th class="text-center w-1/3 font-medium text-sm">Waiting</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="overflow-y-auto rounded-md w-full max-h-[320px] block border bg-white dark:border-immich-dark-gray dark:bg-immich-dark-gray/75 dark:text-immich-dark-fg"
|
||||
>
|
||||
<tr class="text-center flex place-items-center w-full h-[60px]">
|
||||
<td class="text-sm px-2 w-1/3 text-ellipsis">
|
||||
{#if jobCounts}
|
||||
<span>{jobCounts.active > 0 || jobCounts.waiting > 0 ? 'Active' : 'Idle'}</span>
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
</td>
|
||||
<td class="flex justify-center text-sm px-2 w-1/3 text-ellipsis">
|
||||
<div class="flex justify-between rounded-3xl bg-gray-100 dark:bg-immich-dark-gray">
|
||||
<div id="job-info" class="w-[70%] p-9">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-xl font-semibold text-immich-primary dark:text-immich-dark-primary">
|
||||
{title.toUpperCase()}
|
||||
</div>
|
||||
|
||||
{#if subtitle.length > 0}
|
||||
<div class="text-sm dark:text-white">{subtitle}</div>
|
||||
{/if}
|
||||
<div class="text-sm dark:text-white"><slot /></div>
|
||||
|
||||
<div class="flex w-full mt-4">
|
||||
<div
|
||||
class="flex place-items-center justify-between bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray w-full rounded-tl-lg rounded-bl-lg py-4 pl-4 pr-6"
|
||||
>
|
||||
<p>Active</p>
|
||||
<p class="text-2xl">
|
||||
{#if jobCounts.active !== undefined}
|
||||
{jobCounts.active}
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
</td>
|
||||
<td class="flex justify-center text-sm px-2 w-1/3 text-ellipsis">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex place-items-center justify-between bg-gray-200 text-immich-dark-bg dark:bg-gray-700 dark:text-immich-gray w-full rounded-tr-lg rounded-br-lg py-4 pr-4 pl-6"
|
||||
>
|
||||
<p class="text-2xl">
|
||||
{#if jobCounts.waiting !== undefined}
|
||||
{jobCounts.waiting}
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
<p>Waiting</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-[30%] flex place-items-center place-content-end">
|
||||
<button
|
||||
on:click={() => dispatch('click')}
|
||||
class="px-6 py-3 text-sm bg-immich-primary dark:bg-immich-dark-primary font-medium rounded-2xl hover:bg-immich-primary/50 transition-all hover:cursor-pointer disabled:cursor-not-allowed shadow-sm text-immich-bg dark:text-immich-dark-gray"
|
||||
disabled={jobCounts.active > 0 && jobCounts.waiting > 0}
|
||||
>
|
||||
{#if jobCounts.active > 0 || jobCounts.waiting > 0}
|
||||
<div id="job-action" class="flex flex-col">
|
||||
{#if isRunning}
|
||||
<button
|
||||
class="job-play-button bg-gray-300/90 dark:bg-gray-600/90 rounded-br-3xl rounded-tr-3xl disabled:cursor-not-allowed"
|
||||
disabled
|
||||
>
|
||||
<LoadingSpinner />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if !isRunning}
|
||||
{#if showOptions}
|
||||
<button
|
||||
class="job-play-button bg-gray-300 dark:bg-gray-600 rounded-tr-3xl"
|
||||
on:click={() => run(true)}
|
||||
>
|
||||
<AllInclusive size="18" /> ALL
|
||||
</button>
|
||||
<button
|
||||
class="job-play-button bg-gray-300/90 dark:bg-gray-600/90 rounded-br-3xl"
|
||||
on:click={() => run(false)}
|
||||
>
|
||||
<SelectionSearch size="18" /> MISSING
|
||||
</button>
|
||||
{:else}
|
||||
{buttonTitle}
|
||||
<button
|
||||
class="job-play-button bg-gray-300/90 dark:bg-gray-600/90 rounded-br-3xl rounded-tr-3xl"
|
||||
on:click={() => run(true)}
|
||||
>
|
||||
<Play size="48" />
|
||||
</button>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,20 +18,28 @@
|
|||
|
||||
onMount(async () => {
|
||||
await load();
|
||||
timer = setInterval(async () => await load(), 5_000);
|
||||
timer = setInterval(async () => await load(), 1_000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(timer);
|
||||
});
|
||||
|
||||
const run = async (jobId: JobId, jobName: string, emptyMessage: string) => {
|
||||
const run = async (
|
||||
jobId: JobId,
|
||||
jobName: string,
|
||||
emptyMessage: string,
|
||||
includeAllAssets: boolean
|
||||
) => {
|
||||
try {
|
||||
const { data } = await api.jobApi.sendJobCommand(jobId, { command: JobCommand.Start });
|
||||
const { data } = await api.jobApi.sendJobCommand(jobId, {
|
||||
command: JobCommand.Start,
|
||||
includeAllAssets
|
||||
});
|
||||
|
||||
if (data) {
|
||||
notificationController.show({
|
||||
message: `Started ${jobName}`,
|
||||
message: includeAllAssets ? `Started ${jobName} for all assets` : `Started ${jobName}`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} else {
|
||||
|
|
@ -43,53 +51,77 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-10">
|
||||
<div class="flex flex-col gap-7">
|
||||
{#if jobs}
|
||||
<JobTile
|
||||
title={'Generate thumbnails'}
|
||||
subtitle={'Regenerate missing thumbnail (JPEG, WEBP)'}
|
||||
on:click={() =>
|
||||
run(JobId.ThumbnailGeneration, 'thumbnail generation', 'No missing thumbnails found')}
|
||||
subtitle={'Regenerate JPEG and WebP thumbnails'}
|
||||
on:click={(e) => {
|
||||
const { includeAllAssets } = e.detail;
|
||||
|
||||
run(
|
||||
JobId.ThumbnailGeneration,
|
||||
'thumbnail generation',
|
||||
'No missing thumbnails found',
|
||||
includeAllAssets
|
||||
);
|
||||
}}
|
||||
jobCounts={jobs[JobId.ThumbnailGeneration]}
|
||||
/>
|
||||
|
||||
<JobTile
|
||||
title={'Extract EXIF'}
|
||||
subtitle={'Extract missing EXIF information'}
|
||||
on:click={() => run(JobId.MetadataExtraction, 'extract EXIF', 'No missing EXIF found')}
|
||||
title={'EXTRACT METADATA'}
|
||||
subtitle={'Extract metadata information i.e. GPS, resolution...etc'}
|
||||
on:click={(e) => {
|
||||
const { includeAllAssets } = e.detail;
|
||||
run(JobId.MetadataExtraction, 'extract EXIF', 'No missing EXIF found', includeAllAssets);
|
||||
}}
|
||||
jobCounts={jobs[JobId.MetadataExtraction]}
|
||||
/>
|
||||
|
||||
<JobTile
|
||||
title={'Detect objects'}
|
||||
subtitle={'Run machine learning process to detect and classify objects'}
|
||||
on:click={() =>
|
||||
run(JobId.MachineLearning, 'object detection', 'No missing object detection found')}
|
||||
on:click={(e) => {
|
||||
const { includeAllAssets } = e.detail;
|
||||
|
||||
run(
|
||||
JobId.MachineLearning,
|
||||
'object detection',
|
||||
'No missing object detection found',
|
||||
includeAllAssets
|
||||
);
|
||||
}}
|
||||
jobCounts={jobs[JobId.MachineLearning]}
|
||||
>
|
||||
Note that some assets may not have any objects detected, this is normal.
|
||||
Note that some assets may not have any objects detected
|
||||
</JobTile>
|
||||
|
||||
<JobTile
|
||||
title={'Video transcoding'}
|
||||
subtitle={'Run video transcoding process to transcode videos not in the desired format'}
|
||||
on:click={() =>
|
||||
subtitle={'Transcode videos not in the desired format'}
|
||||
on:click={(e) => {
|
||||
const { includeAllAssets } = e.detail;
|
||||
run(
|
||||
JobId.VideoConversion,
|
||||
'video conversion',
|
||||
'No videos without an encoded version found'
|
||||
)}
|
||||
'No videos without an encoded version found',
|
||||
includeAllAssets
|
||||
);
|
||||
}}
|
||||
jobCounts={jobs[JobId.VideoConversion]}
|
||||
/>
|
||||
|
||||
<JobTile
|
||||
title={'Storage migration'}
|
||||
showOptions={false}
|
||||
subtitle={''}
|
||||
on:click={() =>
|
||||
run(
|
||||
JobId.StorageTemplateMigration,
|
||||
'storage template migration',
|
||||
'All files have been migrated to the new storage template'
|
||||
'All files have been migrated to the new storage template',
|
||||
false
|
||||
)}
|
||||
jobCounts={jobs[JobId.StorageTemplateMigration]}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue