library-list.svelte 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. <script lang="ts">
  2. import { api, UpdateLibraryDto, LibraryResponseDto, LibraryType, LibraryStatsResponseDto } from '@api';
  3. import { onMount } from 'svelte';
  4. import Button from '../elements/buttons/button.svelte';
  5. import { notificationController, NotificationType } from '../shared-components/notification/notification';
  6. import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
  7. import { handleError } from '$lib/utils/handle-error';
  8. import { fade } from 'svelte/transition';
  9. import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
  10. import Database from 'svelte-material-icons/Database.svelte';
  11. import Upload from 'svelte-material-icons/Upload.svelte';
  12. import Pulse from 'svelte-loading-spinners/Pulse.svelte';
  13. import { slide } from 'svelte/transition';
  14. import LibraryImportPathsForm from '../forms/library-import-paths-form.svelte';
  15. import LibraryScanSettingsForm from '../forms/library-scan-settings-form.svelte';
  16. import LibraryRenameForm from '../forms/library-rename-form.svelte';
  17. import { getBytesWithUnit } from '$lib/utils/byte-units';
  18. import Portal from '../shared-components/portal/portal.svelte';
  19. import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
  20. import MenuOption from '../shared-components/context-menu/menu-option.svelte';
  21. let libraries: LibraryResponseDto[] = [];
  22. let stats: LibraryStatsResponseDto[] = [];
  23. let photos: number[] = [];
  24. let videos: number[] = [];
  25. let totalCount: number[] = [];
  26. let diskUsage: number[] = [];
  27. let diskUsageUnit: string[] = [];
  28. let confirmDeleteLibrary: LibraryResponseDto | null = null;
  29. let deleteLibrary: LibraryResponseDto | null = null;
  30. let editImportPaths: number | null;
  31. let editScanSettings: number | null;
  32. let renameLibrary: number | null;
  33. let updateLibraryIndex: number | null;
  34. let deleteAssetCount = 0;
  35. let dropdownOpen: boolean[] = [];
  36. let showContextMenu = false;
  37. let contextMenuPosition = { x: 0, y: 0 };
  38. let libraryType: LibraryType;
  39. onMount(() => {
  40. readLibraryList();
  41. });
  42. const closeAll = () => {
  43. editImportPaths = null;
  44. editScanSettings = null;
  45. renameLibrary = null;
  46. updateLibraryIndex = null;
  47. showContextMenu = false;
  48. for (let i = 0; i < dropdownOpen.length; i++) {
  49. dropdownOpen[i] = false;
  50. }
  51. };
  52. const showMenu = ({ x, y }: MouseEvent, type: LibraryType) => {
  53. contextMenuPosition = { x, y };
  54. showContextMenu = !showContextMenu;
  55. libraryType = type;
  56. };
  57. const onMenuExit = () => {
  58. showContextMenu = false;
  59. };
  60. const refreshStats = async (listIndex: number) => {
  61. const { data } = await api.libraryApi.getLibraryStatistics({ id: libraries[listIndex].id });
  62. stats[listIndex] = data;
  63. photos[listIndex] = stats[listIndex].photos;
  64. videos[listIndex] = stats[listIndex].videos;
  65. totalCount[listIndex] = stats[listIndex].total;
  66. [diskUsage[listIndex], diskUsageUnit[listIndex]] = getBytesWithUnit(stats[listIndex].usage, 0);
  67. };
  68. async function readLibraryList() {
  69. const { data } = await api.libraryApi.getAllForUser();
  70. libraries = data;
  71. dropdownOpen.length = libraries.length;
  72. for (let i = 0; i < libraries.length; i++) {
  73. await refreshStats(i);
  74. dropdownOpen[i] = false;
  75. }
  76. }
  77. const handleCreate = async (libraryType: LibraryType) => {
  78. try {
  79. const { data } = await api.libraryApi.createLibrary({
  80. createLibraryDto: { type: libraryType },
  81. });
  82. const createdLibrary = data;
  83. notificationController.show({
  84. message: `Created library: ${createdLibrary.name}`,
  85. type: NotificationType.Info,
  86. });
  87. } catch (error) {
  88. handleError(error, 'Unable to create library');
  89. } finally {
  90. await readLibraryList();
  91. }
  92. };
  93. const handleUpdate = async (event: CustomEvent<UpdateLibraryDto>) => {
  94. if (updateLibraryIndex === null) {
  95. return;
  96. }
  97. try {
  98. const dto = event.detail;
  99. const libraryId = libraries[updateLibraryIndex].id;
  100. await api.libraryApi.updateLibrary({ id: libraryId, updateLibraryDto: dto });
  101. } catch (error) {
  102. handleError(error, 'Unable to update library');
  103. } finally {
  104. closeAll();
  105. await readLibraryList();
  106. }
  107. };
  108. const handleDelete = async () => {
  109. if (confirmDeleteLibrary) {
  110. deleteLibrary = confirmDeleteLibrary;
  111. }
  112. if (!deleteLibrary) {
  113. return;
  114. }
  115. try {
  116. await api.libraryApi.deleteLibrary({ id: deleteLibrary.id });
  117. notificationController.show({
  118. message: `Library deleted`,
  119. type: NotificationType.Info,
  120. });
  121. } catch (error) {
  122. handleError(error, 'Unable to remove library');
  123. } finally {
  124. confirmDeleteLibrary = null;
  125. deleteLibrary = null;
  126. await readLibraryList();
  127. }
  128. };
  129. const handleScanAll = async () => {
  130. try {
  131. for (const library of libraries) {
  132. if (library.type === LibraryType.External) {
  133. await api.libraryApi.scanLibrary({ id: library.id, scanLibraryDto: {} });
  134. }
  135. }
  136. notificationController.show({
  137. message: `Refreshing all libraries`,
  138. type: NotificationType.Info,
  139. });
  140. } catch (error) {
  141. handleError(error, 'Unable to scan libraries');
  142. }
  143. };
  144. const handleScan = async (libraryId: string) => {
  145. try {
  146. await api.libraryApi.scanLibrary({ id: libraryId, scanLibraryDto: {} });
  147. notificationController.show({
  148. message: `Scanning library for new files`,
  149. type: NotificationType.Info,
  150. });
  151. } catch (error) {
  152. handleError(error, 'Unable to scan library');
  153. }
  154. };
  155. const handleScanChanges = async (libraryId: string) => {
  156. try {
  157. await api.libraryApi.scanLibrary({ id: libraryId, scanLibraryDto: { refreshModifiedFiles: true } });
  158. notificationController.show({
  159. message: `Scanning library for changed files`,
  160. type: NotificationType.Info,
  161. });
  162. } catch (error) {
  163. handleError(error, 'Unable to scan library');
  164. }
  165. };
  166. const handleForceScan = async (libraryId: string) => {
  167. try {
  168. await api.libraryApi.scanLibrary({ id: libraryId, scanLibraryDto: { refreshAllFiles: true } });
  169. notificationController.show({
  170. message: `Forcing refresh of all library files`,
  171. type: NotificationType.Info,
  172. });
  173. } catch (error) {
  174. handleError(error, 'Unable to scan library');
  175. }
  176. };
  177. const handleRemoveOffline = async (libraryId: string) => {
  178. try {
  179. await api.libraryApi.removeOfflineFiles({ id: libraryId });
  180. notificationController.show({
  181. message: `Removing Offline Files`,
  182. type: NotificationType.Info,
  183. });
  184. } catch (error) {
  185. handleError(error, 'Unable to remove offline files');
  186. }
  187. };
  188. const onRenameClicked = (index: number) => {
  189. closeAll();
  190. renameLibrary = index;
  191. updateLibraryIndex = index;
  192. };
  193. const onEditImportPathClicked = (index: number) => {
  194. closeAll();
  195. editImportPaths = index;
  196. updateLibraryIndex = index;
  197. };
  198. const onScanNewLibraryClicked = (libraryId: string) => {
  199. closeAll();
  200. handleScan(libraryId);
  201. };
  202. const onScanSettingClicked = (index: number) => {
  203. closeAll();
  204. editScanSettings = index;
  205. updateLibraryIndex = index;
  206. };
  207. const onScanAllLibraryFilesClicked = (libraryId: string) => {
  208. closeAll();
  209. handleScanChanges(libraryId);
  210. };
  211. const onForceScanAllLibraryFilesClicked = (libraryId: string) => {
  212. closeAll();
  213. handleForceScan(libraryId);
  214. };
  215. const onRemoveOfflineFilesClicked = (libraryId: string) => {
  216. closeAll();
  217. handleRemoveOffline(libraryId);
  218. };
  219. const onDeleteLibraryClicked = (index: number, library: LibraryResponseDto) => {
  220. closeAll();
  221. if (confirm(`Are you sure you want to delete ${library.name} library?`) == true) {
  222. refreshStats(index);
  223. if (totalCount[index] > 0) {
  224. deleteAssetCount = totalCount[index];
  225. confirmDeleteLibrary = library;
  226. } else {
  227. deleteLibrary = library;
  228. handleDelete();
  229. }
  230. }
  231. };
  232. </script>
  233. {#if confirmDeleteLibrary}
  234. <ConfirmDialogue
  235. title="Warning!"
  236. prompt="Are you sure you want to delete this library? This will DELETE all {deleteAssetCount} contained assets and cannot be undone."
  237. on:confirm={handleDelete}
  238. on:cancel={() => (confirmDeleteLibrary = null)}
  239. />
  240. {/if}
  241. <section class="my-4">
  242. <div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
  243. {#if libraries.length > 0}
  244. <table class="w-full text-left">
  245. <thead
  246. class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
  247. >
  248. <tr class="flex w-full place-items-center">
  249. <th class="w-1/6 text-center text-sm font-medium">Type</th>
  250. <th class="w-1/3 text-center text-sm font-medium">Name</th>
  251. <th class="w-1/5 text-center text-sm font-medium">Assets</th>
  252. <th class="w-1/6 text-center text-sm font-medium">Size</th>
  253. <th class="w-1/6 text-center text-sm font-medium" />
  254. </tr>
  255. </thead>
  256. <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
  257. {#each libraries as library, index}
  258. {#key library.id}
  259. <tr
  260. class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
  261. index % 2 == 0
  262. ? 'bg-immich-gray dark:bg-immich-dark-gray/75'
  263. : 'bg-immich-bg dark:bg-immich-dark-gray/50'
  264. }`}
  265. >
  266. <td class="w-1/6 px-10 text-sm">
  267. {#if library.type === LibraryType.External}
  268. <Database size="40" title="External library (created on {library.createdAt})" />
  269. {:else if library.type === LibraryType.Upload}
  270. <Upload size="40" title="Upload library (created on {library.createdAt})" />
  271. {/if}</td
  272. >
  273. <td class="w-1/3 text-ellipsis px-4 text-sm">{library.name}</td>
  274. {#if totalCount[index] == undefined}
  275. <td colspan="2" class="flex w-1/3 items-center justify-center text-ellipsis px-4 text-sm">
  276. <Pulse color="gray" size="40" unit="px" />
  277. </td>
  278. {:else}
  279. <td class="w-1/6 text-ellipsis px-4 text-sm">
  280. {totalCount[index]}
  281. </td>
  282. <td class="w-1/6 text-ellipsis px-4 text-sm">{diskUsage[index]} {diskUsageUnit[index]} </td>
  283. {/if}
  284. <td class="w-1/6 text-ellipsis px-4 text-sm">
  285. <button
  286. class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
  287. on:click|stopPropagation|preventDefault={(e) => showMenu(e, library.type)}
  288. >
  289. <DotsVertical size="16" />
  290. </button>
  291. {#if showContextMenu}
  292. <Portal target="body">
  293. <ContextMenu {...contextMenuPosition} on:outclick={() => onMenuExit()}>
  294. <MenuOption on:click={() => onRenameClicked(index)} text="Rename" />
  295. {#if libraryType === LibraryType.External}
  296. <MenuOption on:click={() => onEditImportPathClicked(index)} text="Edit Import Paths" />
  297. <MenuOption on:click={() => onScanSettingClicked(index)} text="Scan Settings" />
  298. <hr />
  299. <MenuOption
  300. on:click={() => onScanNewLibraryClicked(library.id)}
  301. text="Scan New Library Files"
  302. />
  303. <MenuOption
  304. on:click={() => onScanAllLibraryFilesClicked(library.id)}
  305. text="Re-scan All Library Files"
  306. subtitle={'Only refreshes modified files'}
  307. />
  308. <MenuOption
  309. on:click={() => onForceScanAllLibraryFilesClicked(library.id)}
  310. text="Force Re-scan All Library Files"
  311. subtitle={'Refreshes every file'}
  312. />
  313. <hr />
  314. <MenuOption
  315. on:click={() => onRemoveOfflineFilesClicked(library.id)}
  316. text="Remove Offline Files"
  317. />
  318. <MenuOption on:click={() => onDeleteLibraryClicked(index, library)}>
  319. <p class="text-red-600">Delete library</p>
  320. </MenuOption>
  321. {/if}
  322. </ContextMenu>
  323. </Portal>
  324. {/if}
  325. </td>
  326. </tr>
  327. {#if renameLibrary === index}
  328. <div transition:slide={{ duration: 250 }}>
  329. <LibraryRenameForm {library} on:submit={handleUpdate} on:cancel={() => (renameLibrary = null)} />
  330. </div>
  331. {/if}
  332. {#if editImportPaths === index}
  333. <div transition:slide={{ duration: 250 }}>
  334. <LibraryImportPathsForm
  335. {library}
  336. on:submit={handleUpdate}
  337. on:cancel={() => (editImportPaths = null)}
  338. />
  339. </div>
  340. {/if}
  341. {#if editScanSettings === index}
  342. <div transition:slide={{ duration: 250 }} class="mb-4 ml-4 mr-4">
  343. <LibraryScanSettingsForm
  344. {library}
  345. on:submit={handleUpdate}
  346. on:cancel={() => (editScanSettings = null)}
  347. />
  348. </div>
  349. {/if}
  350. {/key}
  351. {/each}
  352. </tbody>
  353. </table>
  354. {/if}
  355. <div class="my-2 flex justify-end gap-2">
  356. <Button size="sm" on:click={() => handleScanAll()}>Scan All Libraries</Button>
  357. <Button size="sm" on:click={() => handleCreate(LibraryType.External)}>Create External Library</Button>
  358. </div>
  359. </div>
  360. </section>