Jason Rasmussen 1 год назад
Родитель
Сommit
e8fdddf08e

+ 1 - 1
web/src/api/api.ts

@@ -38,7 +38,7 @@ export class ImmichApi {
   public personApi: PersonApi;
   public systemConfigApi: SystemConfigApi;
   public userApi: UserApi;
-  
+
   private config: Configuration;
 
   constructor(params: ConfigurationParameters) {

+ 8 - 12
web/src/lib/components/album-page/rule-selection-form/face-selection.svelte

@@ -6,17 +6,15 @@
   import Button from '$lib/components/elements/buttons/button.svelte';
   import FaceThumbnail from '$lib/components/assets/thumbnail/face-thumbnail.svelte';
 
-  export let selectedPeople: Set<PersonResponseDto> = new Set();
+  export let selectedIds: string[] = [];
   let people: PersonResponseDto[] = [];
   let newPeople: PersonResponseDto[] = [];
 
-  const dispatch = createEventDispatcher<{ close: void; confirm: { people: PersonResponseDto[] } }>();
+  const dispatch = createEventDispatcher<{ close: void; confirm: PersonResponseDto[] }>();
 
   onMount(async () => {
     const { data } = await api.personApi.getAllPeople({ withHidden: false });
-
-    const selectedPeopleIds = Array.from(selectedPeople).map((p) => p.id);
-    people = data.people.filter((p) => !selectedPeopleIds.includes(p.id));
+    people = data.people.filter(({ id }) => !selectedIds.includes(id));
   });
 
   const handleSelection = (e: CustomEvent<{ person: PersonResponseDto }>) => {
@@ -28,15 +26,11 @@
       newPeople = [...newPeople, person];
     }
   };
-
-  const onConfirmClicked = () => {
-    dispatch('confirm', { people: newPeople });
-  };
 </script>
 
 <ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => dispatch('close')}>
   <svelte:fragment slot="leading">
-    <p class="text-immich-fg dark:text-immich-dark-fg font-medium">
+    <p class="font-medium text-immich-fg dark:text-immich-dark-fg">
       {#if newPeople.length === 0}
         Select faces
       {:else if newPeople.length === 1}
@@ -48,12 +42,14 @@
   </svelte:fragment>
 
   <svelte:fragment slot="trailing">
-    <Button disabled={newPeople.length === 0} size="sm" title="Confirm" on:click={onConfirmClicked}>Confirm</Button>
+    <Button disabled={newPeople.length === 0} size="sm" title="Confirm" on:click={() => dispatch('confirm', newPeople)}
+      >Confirm</Button
+    >
   </svelte:fragment>
 </ControlAppBar>
 
 <div class="mt-24 flex flex-wrap gap-2 px-8">
-  {#each people as person}
+  {#each people as person (person.id)}
     <FaceThumbnail
       {person}
       thumbnailSize={180}

+ 25 - 48
web/src/lib/components/album-page/rule-selection-form/rule-selection-form.svelte

@@ -1,60 +1,33 @@
 <script lang="ts">
   import BaseModal from '$lib/components/shared-components/base-modal.svelte';
-  import { createEventDispatcher, onMount } from 'svelte';
-  import { RuleKey, type AlbumResponseDto, type PersonResponseDto, api } from '@api';
+  import { PersonResponseDto, RuleKey, RuleResponseDto, api } from '@api';
+  import { createEventDispatcher } from 'svelte';
   import Plus from 'svelte-material-icons/Plus.svelte';
+  import { fly } from 'svelte/transition';
   import Button from '../../elements/buttons/button.svelte';
   import Portal from '../../shared-components/portal/portal.svelte';
   import FaceSelection from './face-selection.svelte';
-  import { fly } from 'svelte/transition';
 
-  export let album: AlbumResponseDto;
+  export let rules: RuleResponseDto[] = [];
 
   let peopleSelection = false;
   let locationSelection = false;
-  let selectedPeople = new Set<PersonResponseDto>();
 
-  const dispatch = createEventDispatcher<{ close: void }>();
+  $: peopleRules = rules.filter((rule) => rule.key === RuleKey.Person);
 
-  const handlePeopleSelected = async (e: CustomEvent<{ people: PersonResponseDto[] }>) => {
-    peopleSelection = false;
-    const people = e.detail.people;
-
-    selectedPeople = new Set([...selectedPeople, ...people]);
-  };
+  const dispatch = createEventDispatcher<{
+    submit: RuleResponseDto[];
+    close: void;
+  }>();
 
-  const handleRemovePerson = (person: PersonResponseDto) => {
-    const temp = new Set(selectedPeople);
-    temp.delete(person);
-    selectedPeople = temp;
+  const handleSelectPeople = async (people: PersonResponseDto[]) => {
+    rules = [...rules, ...people.map((person) => ({ key: RuleKey.Person, value: person.id } as RuleResponseDto))];
+    peopleSelection = false;
   };
 
-  const updateRule = async () => {
-    // for (const person of people) {
-    //   const { data } = await api.ruleApi.createRule({
-    //     createRuleDto: {
-    //       albumId: album.id,
-    //       key: RuleKey.Person,
-    //       value: person.id,
-    //     },
-    //   });
-    //   album.rules = [...album.rules, data];
-    // }
+  const handleRemoveRule = async (rule: RuleResponseDto) => {
+    rules = rules.filter((_rule) => rule !== _rule);
   };
-
-  onMount(async () => {
-    const addedPeople: PersonResponseDto[] = [];
-
-    for (const rule of album.rules) {
-      if (rule.key === RuleKey.Person) {
-        const personId = String(rule.value);
-        const { data } = await api.personApi.getPerson({ id: personId });
-        addedPeople.push(data);
-      }
-    }
-
-    selectedPeople = new Set([...selectedPeople, ...addedPeople]);
-  });
 </script>
 
 <BaseModal
@@ -65,7 +38,7 @@
 >
   <svelte:fragment slot="title">
     <div class="flex place-items-center gap-2">
-      <p class="text-immich-fg dark:text-immich-dark-fg font-medium">Automatically add photos</p>
+      <p class="font-medium text-immich-fg dark:text-immich-dark-fg">Automatically add photos</p>
     </div>
   </svelte:fragment>
 
@@ -75,9 +48,9 @@
       <p class="text-sm">PEOPLE</p>
 
       <div class="mt-4 flex flex-wrap gap-2">
-        {#each selectedPeople as person (person.id)}
-          <button on:click={() => handleRemovePerson(person)}>
-            <img src={api.getPeopleThumbnailUrl(person.id)} alt={person.id} class="h-20 w-20 rounded-lg" />
+        {#each peopleRules as rule}
+          <button on:click={() => handleRemoveRule(rule)}>
+            <img src={api.getPeopleThumbnailUrl(rule.value)} alt={rule.value} class="h-20 w-20 rounded-lg" />
           </button>
         {/each}
 
@@ -122,7 +95,7 @@
   <svelte:fragment slot="sticky-bottom">
     <div class="flex justify-end gap-2">
       <Button size="sm" color="secondary" on:click={() => dispatch('close')}>Cancel</Button>
-      <Button size="sm" color="primary">Confirm</Button>
+      <Button size="sm" color="primary" on:click={() => dispatch('submit', rules)}>Confirm</Button>
     </div>
   </svelte:fragment>
 </BaseModal>
@@ -131,9 +104,13 @@
   {#if peopleSelection}
     <section
       transition:fly={{ y: 500 }}
-      class="dark:bg-immich-dark-bg absolute left-0 top-0 z-[10000] h-full min-h-max w-full overflow-y-auto bg-gray-200"
+      class="absolute left-0 top-0 z-[10000] h-full min-h-max w-full overflow-y-auto bg-gray-200 dark:bg-immich-dark-bg"
     >
-      <FaceSelection on:close={() => (peopleSelection = false)} on:confirm={handlePeopleSelected} {selectedPeople} />
+      <FaceSelection
+        on:close={() => (peopleSelection = false)}
+        on:confirm={({ detail: people }) => handleSelectPeople(people)}
+        selectedIds={peopleRules.map(({ value }) => value)}
+      />
     </section>
   {/if}
 </Portal>

+ 2 - 2
web/src/lib/components/assets/thumbnail/face-thumbnail.svelte

@@ -100,7 +100,7 @@
       </div>
 
       <div
-        class="dark:bg-immich-dark-gray absolute h-full w-full select-none bg-gray-100 transition-transform"
+        class="absolute h-full w-full select-none bg-gray-100 transition-transform dark:bg-immich-dark-gray"
         class:scale-[0.85]={selected}
         class:rounded-xl={selected}
       >
@@ -120,7 +120,7 @@
       </div>
       {#if selectionCandidate}
         <div
-          class="bg-immich-primary absolute top-0 h-full w-full opacity-40"
+          class="absolute top-0 h-full w-full bg-immich-primary opacity-40"
           in:fade={{ duration: 100 }}
           out:fade={{ duration: 100 }}
         />

+ 3 - 3
web/src/lib/components/shared-components/base-modal.svelte

@@ -37,9 +37,9 @@
   <div
     use:clickOutside
     on:outclick={() => !ignoreClickOutside && dispatch('close')}
-    class="bg-immich-bg dark:bg-immich-dark-gray dark:text-immich-dark-fg max-h-[700px] min-h-[200px] w-[450px] overflow-y-auto rounded-lg shadow-md"
+    class="max-h-[700px] min-h-[200px] w-[450px] overflow-y-auto rounded-lg bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg"
   >
-    <div class="dark:bg-immich-dark-gray bg-immich-bg sticky top-0 flex place-items-center justify-between px-5 py-3">
+    <div class="sticky top-0 flex place-items-center justify-between bg-immich-bg px-5 py-3 dark:bg-immich-dark-gray">
       <div>
         <slot name="title">
           <p>Modal Title</p>
@@ -54,7 +54,7 @@
     </div>
 
     {#if $$slots['sticky-bottom']}
-      <div class="dark:bg-immich-dark-gray bg-immich-bg sticky bottom-0 px-5 pb-5 pt-3">
+      <div class="sticky bottom-0 bg-immich-bg px-5 pb-5 pt-3 dark:bg-immich-dark-gray">
         <slot name="sticky-bottom" />
       </div>
     {/if}

+ 36 - 10
web/src/routes/(user)/albums/[albumId]/+page.svelte

@@ -33,7 +33,7 @@
   import { downloadArchive } from '$lib/utils/asset-utils';
   import { openFileUploadDialog } from '$lib/utils/file-uploader';
   import { handleError } from '$lib/utils/handle-error';
-  import { TimeBucketSize, UserResponseDto, api } from '@api';
+  import { RuleResponseDto, TimeBucketSize, UserResponseDto, api } from '@api';
   import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
   import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
   import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
@@ -279,6 +279,28 @@
       handleError(error, 'Error updating album description');
     }
   };
+
+  const handleUpdateRules = async (rules: RuleResponseDto[]) => {
+    let ids = rules.filter((rule) => !!rule.id).map((rule) => rule.id);
+
+    for (const rule of album.rules) {
+      if (!ids.includes(rule.id)) {
+        await api.ruleApi.removeRule({ id: rule.id });
+      }
+    }
+
+    for (const { id, key, value } of rules) {
+      if (!id) {
+        await api.ruleApi.createRule({ createRuleDto: { albumId: album.id, key, value } });
+      } else {
+        await api.ruleApi.updateRule({ id, updateRuleDto: { key, value } });
+      }
+    }
+
+    await refreshAlbum();
+
+    viewMode = ViewMode.VIEW;
+  };
 </script>
 
 <header>
@@ -354,7 +376,7 @@
     {#if viewMode === ViewMode.SELECT_ASSETS}
       <ControlAppBar on:close-button-click={handleCloseSelectAssets}>
         <svelte:fragment slot="leading">
-          <p class="dark:text-immich-dark-fg text-lg">
+          <p class="text-lg dark:text-immich-dark-fg">
             {#if $timelineSelected.size === 0}
               Add to album
             {:else}
@@ -366,7 +388,7 @@
         <svelte:fragment slot="trailing">
           <button
             on:click={handleSelectFromComputer}
-            class="text-immich-primary hover:bg-immich-primary/10 dark:text-immich-dark-primary dark:hover:bg-immich-dark-primary/25 rounded-lg px-6 py-2 text-sm font-medium transition-all"
+            class="rounded-lg px-6 py-2 text-sm font-medium text-immich-primary transition-all hover:bg-immich-primary/10 dark:text-immich-dark-primary dark:hover:bg-immich-dark-primary/25"
           >
             Select from computer
           </button>
@@ -385,7 +407,7 @@
 </header>
 
 <main
-  class="bg-immich-bg dark:bg-immich-dark-bg relative h-screen overflow-hidden px-6 pt-[var(--navbar-height)] sm:px-12 md:px-24 lg:px-40"
+  class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40"
 >
   {#if viewMode === ViewMode.SELECT_ASSETS}
     <AssetGrid assetStore={timelineStore} assetInteractionStore={timelineInteractionStore} isSelectionMode={true} />
@@ -403,9 +425,9 @@
           <input
             on:keydown={(e) => e.key === 'Enter' && titleInput.blur()}
             on:blur={handleUpdateName}
-            class="text-immich-primary dark:text-immich-dark-primary w-[99%] border-b-2 border-transparent text-6xl outline-none transition-all {isOwned
+            class="w-[99%] border-b-2 border-transparent text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned
               ? 'hover:border-gray-400'
-              : 'hover:border-transparent'} bg-immich-bg focus:border-immich-primary dark:bg-immich-dark-bg dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray focus:border-b-2 focus:outline-none"
+              : 'hover:border-transparent'} bg-immich-bg focus:border-b-2 focus:border-immich-primary focus:outline-none dark:bg-immich-dark-bg dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray"
             type="text"
             bind:value={album.albumName}
             disabled={!isOwned}
@@ -479,11 +501,11 @@
       {#if album.assetCount === 0}
         <section id="empty-album" class=" mt-[200px] flex flex-col place-content-center place-items-center">
           <div class="w-[340px]">
-            <p class="dark:text-immich-dark-fg text-sm">ADD PHOTOS</p>
+            <p class="text-sm dark:text-immich-dark-fg">ADD PHOTOS</p>
 
             <button
               on:click={() => (viewMode = ViewMode.RULE_SELECTION)}
-              class="bg-immich-bg text-immich-fg hover:text-immich-primary dark:bg-immich-dark-gray dark:text-immich-dark-fg dark:hover:text-immich-dark-primary mt-5 flex w-full place-items-center gap-6 rounded-md border px-8 py-8 transition-all hover:bg-gray-100 dark:border-none"
+              class="mt-5 flex w-full place-items-center gap-6 rounded-md border bg-immich-bg px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none dark:bg-immich-dark-gray dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
             >
               <span class="immich-text-primary"><FaceMan size="34" /> </span>
               <div class="text-left">
@@ -494,7 +516,7 @@
 
             <button
               on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
-              class="bg-immich-bg text-immich-fg hover:text-immich-primary dark:bg-immich-dark-gray dark:text-immich-dark-fg dark:hover:text-immich-dark-primary mt-5 flex w-full place-items-center gap-6 rounded-md border px-8 py-8 transition-all hover:bg-gray-100 dark:border-none"
+              class="mt-5 flex w-full place-items-center gap-6 rounded-md border bg-immich-bg px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none dark:bg-immich-dark-gray dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
             >
               <span class="immich-text-primary"><Plus size="34" /> </span>
               <span class="text-lg">Select photos</span>
@@ -550,5 +572,9 @@
 {/if}
 
 {#if viewMode === ViewMode.RULE_SELECTION}
-  <RuleSelection on:close={() => (viewMode = ViewMode.VIEW)} {album} />
+  <RuleSelection
+    on:close={() => (viewMode = ViewMode.VIEW)}
+    rules={album.rules}
+    on:submit={({ detail: rules }) => handleUpdateRules(rules)}
+  />
 {/if}