Pārlūkot izejas kodu

refactor(web): material icons (#4636)

Jason Rasmussen 1 gadu atpakaļ
vecāks
revīzija
2ad389f64e
89 mainītis faili ar 557 papildinājumiem un 534 dzēšanām
  1. 1 1
      web/jest.config.mjs
  2. 11 15
      web/package-lock.json
  3. 1 1
      web/package.json
  4. 19 17
      web/src/lib/components/admin-page/jobs/job-tile.svelte
  5. 22 21
      web/src/lib/components/admin-page/jobs/jobs-panel.svelte
  6. 9 10
      web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte
  7. 3 3
      web/src/lib/components/admin-page/server-stats/stats-card.svelte
  8. 3 2
      web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte
  9. 3 2
      web/src/lib/components/album-page/album-card.svelte
  10. 3 4
      web/src/lib/components/album-page/album-viewer.svelte
  11. 2 2
      web/src/lib/components/album-page/share-info-modal.svelte
  12. 4 4
      web/src/lib/components/album-page/user-selection-modal.svelte
  13. 28 26
      web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
  14. 9 14
      web/src/lib/components/asset-viewer/asset-viewer.svelte
  15. 7 10
      web/src/lib/components/asset-viewer/detail-panel.svelte
  16. 2 2
      web/src/lib/components/asset-viewer/download-panel.svelte
  17. 4 3
      web/src/lib/components/assets/thumbnail/image-thumbnail.svelte
  18. 19 16
      web/src/lib/components/assets/thumbnail/thumbnail.svelte
  19. 7 8
      web/src/lib/components/assets/thumbnail/video-thumbnail.svelte
  20. 3 3
      web/src/lib/components/elements/buttons/circle-icon-button.svelte
  21. 7 5
      web/src/lib/components/elements/dropdown.svelte
  22. 36 0
      web/src/lib/components/elements/icon.svelte
  23. 5 6
      web/src/lib/components/faces-page/merge-face-selector.svelte
  24. 5 6
      web/src/lib/components/faces-page/merge-suggestion-modal.svelte
  25. 3 2
      web/src/lib/components/faces-page/people-card.svelte
  26. 3 2
      web/src/lib/components/faces-page/set-birth-date-modal.svelte
  27. 8 7
      web/src/lib/components/faces-page/show-hide.svelte
  28. 3 2
      web/src/lib/components/forms/api-key-form.svelte
  29. 3 2
      web/src/lib/components/forms/api-key-secret.svelte
  30. 3 4
      web/src/lib/components/forms/edit-album-form.svelte
  31. 3 2
      web/src/lib/components/forms/edit-user-form.svelte
  32. 3 2
      web/src/lib/components/forms/library-exclusion-pattern-form.svelte
  33. 3 2
      web/src/lib/components/forms/library-import-path-form.svelte
  34. 3 2
      web/src/lib/components/forms/library-import-paths-form.svelte
  35. 3 2
      web/src/lib/components/forms/library-scan-settings-form.svelte
  36. 6 11
      web/src/lib/components/memory-page/memory-viewer.svelte
  37. 4 6
      web/src/lib/components/photos-page/actions/archive-action.svelte
  38. 3 3
      web/src/lib/components/photos-page/actions/create-shared-link.svelte
  39. 3 5
      web/src/lib/components/photos-page/actions/delete-assets.svelte
  40. 2 2
      web/src/lib/components/photos-page/actions/download-action.svelte
  41. 4 6
      web/src/lib/components/photos-page/actions/favorite-action.svelte
  42. 2 2
      web/src/lib/components/photos-page/actions/remove-from-album.svelte
  43. 2 2
      web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte
  44. 3 2
      web/src/lib/components/photos-page/actions/restore-assets.svelte
  45. 3 4
      web/src/lib/components/photos-page/actions/select-all-assets.svelte
  46. 4 4
      web/src/lib/components/photos-page/asset-date-group.svelte
  47. 2 3
      web/src/lib/components/photos-page/asset-select-context-menu.svelte
  48. 2 2
      web/src/lib/components/photos-page/asset-select-control-bar.svelte
  49. 4 4
      web/src/lib/components/photos-page/memory-lane.svelte
  50. 5 8
      web/src/lib/components/share-page/individual-shared-viewer.svelte
  51. 3 2
      web/src/lib/components/shared-components/album-selection-modal.svelte
  52. 2 2
      web/src/lib/components/shared-components/base-modal.svelte
  53. 3 3
      web/src/lib/components/shared-components/control-app-bar.svelte
  54. 3 2
      web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
  55. 4 4
      web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte
  56. 6 6
      web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
  57. 5 7
      web/src/lib/components/shared-components/notification/notification-card.svelte
  58. 8 6
      web/src/lib/components/shared-components/search-bar/search-bar.svelte
  59. 2 2
      web/src/lib/components/shared-components/show-shortcuts.svelte
  60. 6 10
      web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte
  61. 6 6
      web/src/lib/components/shared-components/side-bar/side-bar-button.svelte
  62. 30 23
      web/src/lib/components/shared-components/side-bar/side-bar.svelte
  63. 4 4
      web/src/lib/components/shared-components/status-box.svelte
  64. 4 4
      web/src/lib/components/shared-components/upload-asset-preview.svelte
  65. 6 8
      web/src/lib/components/shared-components/upload-panel.svelte
  66. 6 8
      web/src/lib/components/sharedlinks-page/shared-link-card.svelte
  67. 19 16
      web/src/lib/components/user-settings-page/device-card.svelte
  68. 5 6
      web/src/lib/components/user-settings-page/library-list.svelte
  69. 2 2
      web/src/lib/components/user-settings-page/partner-settings.svelte
  70. 4 4
      web/src/lib/components/user-settings-page/user-api-key-list.svelte
  71. 18 15
      web/src/routes/(user)/albums/+page.svelte
  72. 24 19
      web/src/routes/(user)/albums/[albumId]/+page.svelte
  73. 3 4
      web/src/routes/(user)/archive/+page.svelte
  74. 13 10
      web/src/routes/(user)/explore/+page.svelte
  75. 3 4
      web/src/routes/(user)/favorites/+page.svelte
  76. 3 2
      web/src/routes/(user)/map/+page.svelte
  77. 3 4
      web/src/routes/(user)/partners/[userId]/+page.svelte
  78. 5 5
      web/src/routes/(user)/people/+page.svelte
  79. 5 7
      web/src/routes/(user)/people/[personId]/+page.svelte
  80. 3 4
      web/src/routes/(user)/photos/+page.svelte
  81. 7 10
      web/src/routes/(user)/search/+page.svelte
  82. 4 4
      web/src/routes/(user)/sharing/+page.svelte
  83. 2 2
      web/src/routes/(user)/sharing/sharedlinks/+page.svelte
  84. 4 4
      web/src/routes/(user)/trash/+page.svelte
  85. 6 8
      web/src/routes/+error.svelte
  86. 3 2
      web/src/routes/admin/jobs-status/+page.svelte
  87. 8 11
      web/src/routes/admin/repair/+page.svelte
  88. 5 6
      web/src/routes/admin/system-settings/+page.svelte
  89. 10 14
      web/src/routes/admin/user-management/+page.svelte

+ 1 - 1
web/jest.config.mjs

@@ -186,7 +186,7 @@ export default {
   },
 
   // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
-  transformIgnorePatterns: ['/node_modules/(?!svelte-material-icons).*/', '\\.pnp\\.[^\\/]+$'],
+  transformIgnorePatterns: ['\\.pnp\\.[^\\/]+$'],
 
   // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
   // unmockedModulePathPatterns: undefined,

+ 11 - 15
web/package-lock.json

@@ -9,6 +9,7 @@
       "version": "1.0.0",
       "dependencies": {
         "@egjs/svelte-view360": "^4.0.0-beta.7",
+        "@mdi/js": "^7.3.67",
         "@zoom-image/svelte": "^0.1.8",
         "axios": "^0.27.2",
         "buffer": "^6.0.3",
@@ -23,7 +24,6 @@
         "socket.io-client": "^4.6.1",
         "svelte-loading-spinners": "^0.3.4",
         "svelte-local-storage-store": "^0.5.0",
-        "svelte-material-icons": "^3.0.5",
         "thumbhash": "^0.1.1"
       },
       "devDependencies": {
@@ -3197,6 +3197,11 @@
         "@jridgewell/sourcemap-codec": "^1.4.14"
       }
     },
+    "node_modules/@mdi/js": {
+      "version": "7.3.67",
+      "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.3.67.tgz",
+      "integrity": "sha512-MnRjknFqpTC6FifhGHjZ0+QYq2bAkZFQqIj8JA2AdPZbBxUvr8QSgB2yPAJ8/ob/XkR41xlg5majDR3c1JP1hw=="
+    },
     "node_modules/@namnode/store": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/@namnode/store/-/store-0.1.0.tgz",
@@ -11418,14 +11423,6 @@
         "svelte": "^3.48.0 || ^4.0.0"
       }
     },
-    "node_modules/svelte-material-icons": {
-      "version": "3.0.5",
-      "resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-3.0.5.tgz",
-      "integrity": "sha512-UbhAa+Btd5y6e6DMljVccP+cbJ8lvesltMippiCOvfIUtYe2TsQqM+P6osfrVsZHV47b1tY6AmqCuSpMKnwMOQ==",
-      "peerDependencies": {
-        "svelte": "^3.0.0 || ^4.0.0"
-      }
-    },
     "node_modules/svelte-preprocess": {
       "version": "5.0.4",
       "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.4.tgz",
@@ -14495,6 +14492,11 @@
         "@jridgewell/sourcemap-codec": "^1.4.14"
       }
     },
+    "@mdi/js": {
+      "version": "7.3.67",
+      "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.3.67.tgz",
+      "integrity": "sha512-MnRjknFqpTC6FifhGHjZ0+QYq2bAkZFQqIj8JA2AdPZbBxUvr8QSgB2yPAJ8/ob/XkR41xlg5majDR3c1JP1hw=="
+    },
     "@namnode/store": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/@namnode/store/-/store-0.1.0.tgz",
@@ -20512,12 +20514,6 @@
       "integrity": "sha512-SEDrpapeia6fUqta+r1NvSLlJYPkZ4pBcl15EYIOSPNzy6vhpoXu8cnzUDmZxsWl7fZGAHxrVH9UyZCbyO4W+g==",
       "requires": {}
     },
-    "svelte-material-icons": {
-      "version": "3.0.5",
-      "resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-3.0.5.tgz",
-      "integrity": "sha512-UbhAa+Btd5y6e6DMljVccP+cbJ8lvesltMippiCOvfIUtYe2TsQqM+P6osfrVsZHV47b1tY6AmqCuSpMKnwMOQ==",
-      "requires": {}
-    },
     "svelte-preprocess": {
       "version": "5.0.4",
       "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.4.tgz",

+ 1 - 1
web/package.json

@@ -62,6 +62,7 @@
   "type": "module",
   "dependencies": {
     "@egjs/svelte-view360": "^4.0.0-beta.7",
+    "@mdi/js": "^7.3.67",
     "@zoom-image/svelte": "^0.1.8",
     "axios": "^0.27.2",
     "buffer": "^6.0.3",
@@ -76,7 +77,6 @@
     "socket.io-client": "^4.6.1",
     "svelte-loading-spinners": "^0.3.4",
     "svelte-local-storage-store": "^0.5.0",
-    "svelte-material-icons": "^3.0.5",
     "thumbhash": "^0.1.1"
   }
 }

+ 19 - 17
web/src/lib/components/admin-page/jobs/job-tile.svelte

@@ -1,25 +1,27 @@
 <script lang="ts">
-  import type Icon from 'svelte-material-icons/AbTesting.svelte';
-  import SelectionSearch from 'svelte-material-icons/SelectionSearch.svelte';
-  import Play from 'svelte-material-icons/Play.svelte';
-  import Pause from 'svelte-material-icons/Pause.svelte';
-  import FastForward from 'svelte-material-icons/FastForward.svelte';
-  import AllInclusive from 'svelte-material-icons/AllInclusive.svelte';
-  import Close from 'svelte-material-icons/Close.svelte';
-  import AlertCircle from 'svelte-material-icons/AlertCircle.svelte';
   import { locale } from '$lib/stores/preferences.store';
   import { createEventDispatcher } from 'svelte';
   import { JobCommand, JobCommandDto, JobCountsDto, QueueStatusDto } from '@api';
   import Badge from '$lib/components/elements/badge.svelte';
   import JobTileButton from './job-tile-button.svelte';
   import JobTileStatus from './job-tile-status.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
+  import {
+    mdiAlertCircle,
+    mdiAllInclusive,
+    mdiClose,
+    mdiFastForward,
+    mdiPause,
+    mdiPlay,
+    mdiSelectionSearch,
+  } from '@mdi/js';
 
   export let title: string;
   export let subtitle: string | undefined = undefined;
   export let jobCounts: JobCountsDto;
   export let queueStatus: QueueStatusDto;
   export let allowForceCommand = true;
-  export let icon: typeof Icon;
+  export let icon: string;
   export let disabled = false;
 
   export let allText: string;
@@ -47,7 +49,7 @@
     <div class="flex flex-col gap-2 p-5 sm:p-7 md:p-9">
       <div class="flex items-center gap-4 text-xl font-semibold text-immich-primary dark:text-immich-dark-primary">
         <span class="flex items-center gap-2">
-          <svelte:component this={icon} size="1.25em" class="hidden shrink-0 sm:block" />
+          <Icon path={icon} size="1.25em" class="hidden shrink-0 sm:block" />
           {title.toUpperCase()}
         </span>
         <div class="flex gap-2">
@@ -102,12 +104,12 @@
         color="light-gray"
         on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
       >
-        <AlertCircle size="36" /> DISABLED
+        <Icon path={mdiAlertCircle} size="36" /> DISABLED
       </JobTileButton>
     {:else if !isIdle}
       {#if waitingCount > 0}
         <JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Empty, force: false })}>
-          <Close size="24" /> CLEAR
+          <Icon path={mdiClose} size="24" /> CLEAR
         </JobTileButton>
       {/if}
       {#if queueStatus.isPaused}
@@ -117,26 +119,26 @@
           on:click={() => dispatch('command', { command: JobCommand.Resume, force: false })}
         >
           <!-- size property is not reactive, so have to use width and height -->
-          <FastForward width={size} height={size} /> RESUME
+          <Icon path={mdiFastForward} {size} /> RESUME
         </JobTileButton>
       {:else}
         <JobTileButton
           color="light-gray"
           on:click={() => dispatch('command', { command: JobCommand.Pause, force: false })}
         >
-          <Pause size="24" /> PAUSE
+          <Icon path={mdiPause} size="24" /> PAUSE
         </JobTileButton>
       {/if}
     {:else if allowForceCommand}
       <JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Start, force: true })}>
-        <AllInclusive size="24" />
+        <Icon path={mdiAllInclusive} size="24" />
         {allText}
       </JobTileButton>
       <JobTileButton
         color="light-gray"
         on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
       >
-        <SelectionSearch size="24" />
+        <Icon path={mdiSelectionSearch} size="24" />
         {missingText}
       </JobTileButton>
     {:else}
@@ -144,7 +146,7 @@
         color="light-gray"
         on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
       >
-        <Play size="48" /> START
+        <Icon path={mdiPlay} size="48" /> START
       </JobTileButton>
     {/if}
   </div>

+ 22 - 21
web/src/lib/components/admin-page/jobs/jobs-panel.svelte

@@ -7,16 +7,17 @@
   import { handleError } from '$lib/utils/handle-error';
   import { AllJobStatusResponseDto, api, JobCommand, JobCommandDto, JobName } from '@api';
   import type { ComponentType } from 'svelte';
-  import type Icon from 'svelte-material-icons/DotsVertical.svelte';
-  import FaceRecognition from 'svelte-material-icons/FaceRecognition.svelte';
-  import FileJpgBox from 'svelte-material-icons/FileJpgBox.svelte';
-  import FileXmlBox from 'svelte-material-icons/FileXmlBox.svelte';
-  import LibraryShelves from 'svelte-material-icons/LibraryShelves.svelte';
-  import FolderMove from 'svelte-material-icons/FolderMove.svelte';
-  import Table from 'svelte-material-icons/Table.svelte';
-  import TagMultiple from 'svelte-material-icons/TagMultiple.svelte';
-  import VectorCircle from 'svelte-material-icons/VectorCircle.svelte';
-  import Video from 'svelte-material-icons/Video.svelte';
+  import {
+    mdiFaceRecognition,
+    mdiFileJpgBox,
+    mdiFileXmlBox,
+    mdiFolderMove,
+    mdiLibraryShelves,
+    mdiTable,
+    mdiTagMultiple,
+    mdiVectorCircle,
+    mdiVideo,
+  } from '@mdi/js';
   import ConfirmDialogue from '../../shared-components/confirm-dialogue.svelte';
   import JobTile from './job-tile.svelte';
   import StorageMigrationDescription from './storage-migration-description.svelte';
@@ -29,7 +30,7 @@
     allText?: string;
     missingText?: string;
     disabled?: boolean;
-    icon: typeof Icon;
+    icon: string;
     allowForceCommand?: boolean;
     component?: ComponentType;
     handleCommand?: (jobId: JobName, jobCommand: JobCommandDto) => Promise<void>;
@@ -53,17 +54,17 @@
 
   $: jobDetails = <Partial<Record<JobName, JobDetails>>>{
     [JobName.ThumbnailGeneration]: {
-      icon: FileJpgBox,
+      icon: mdiFileJpgBox,
       title: api.getJobName(JobName.ThumbnailGeneration),
       subtitle: 'Regenerate JPEG and WebP thumbnails',
     },
     [JobName.MetadataExtraction]: {
-      icon: Table,
+      icon: mdiTable,
       title: api.getJobName(JobName.MetadataExtraction),
       subtitle: 'Extract metadata information i.e. GPS, resolution...etc',
     },
     [JobName.Library]: {
-      icon: LibraryShelves,
+      icon: mdiLibraryShelves,
       title: api.getJobName(JobName.Library),
       subtitle: 'Perform library tasks',
       allText: 'ALL',
@@ -71,44 +72,44 @@
     },
     [JobName.Sidecar]: {
       title: api.getJobName(JobName.Sidecar),
-      icon: FileXmlBox,
+      icon: mdiFileXmlBox,
       subtitle: 'Discover or synchronize sidecar metadata from the filesystem',
       allText: 'SYNC',
       missingText: 'DISCOVER',
       disabled: !$featureFlags.sidecar,
     },
     [JobName.ObjectTagging]: {
-      icon: TagMultiple,
+      icon: mdiTagMultiple,
       title: api.getJobName(JobName.ObjectTagging),
       subtitle: 'Run machine learning to tag objects\nNote that some assets may not have any objects detected',
       disabled: !$featureFlags.tagImage,
     },
     [JobName.ClipEncoding]: {
-      icon: VectorCircle,
+      icon: mdiVectorCircle,
       title: api.getJobName(JobName.ClipEncoding),
       subtitle: 'Run machine learning to generate clip embeddings',
       disabled: !$featureFlags.clipEncode,
     },
     [JobName.RecognizeFaces]: {
-      icon: FaceRecognition,
+      icon: mdiFaceRecognition,
       title: api.getJobName(JobName.RecognizeFaces),
       subtitle: 'Run machine learning to recognize faces',
       handleCommand: handleFaceCommand,
       disabled: !$featureFlags.facialRecognition,
     },
     [JobName.VideoConversion]: {
-      icon: Video,
+      icon: mdiVideo,
       title: api.getJobName(JobName.VideoConversion),
       subtitle: 'Transcode videos not in the desired format',
     },
     [JobName.StorageTemplateMigration]: {
-      icon: FolderMove,
+      icon: mdiFolderMove,
       title: api.getJobName(JobName.StorageTemplateMigration),
       allowForceCommand: false,
       component: StorageMigrationDescription,
     },
     [JobName.Migration]: {
-      icon: FolderMove,
+      icon: mdiFolderMove,
       title: api.getJobName(JobName.Migration),
       subtitle: 'Migrate thumbnails for assets and faces to the latest folder structure',
       allowForceCommand: false,

+ 9 - 10
web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte

@@ -1,11 +1,10 @@
 <script lang="ts">
   import { locale } from '$lib/stores/preferences.store';
   import type { ServerStatsResponseDto } from '@api';
-  import CameraIris from 'svelte-material-icons/CameraIris.svelte';
-  import Memory from 'svelte-material-icons/Memory.svelte';
-  import PlayCircle from 'svelte-material-icons/PlayCircle.svelte';
-  import { asByteUnitString, getBytesWithUnit } from '../../../utils/byte-units';
+  import { asByteUnitString, getBytesWithUnit } from '$lib/utils/byte-units';
   import StatsCard from './stats-card.svelte';
+  import { mdiCameraIris, mdiMemory, mdiPlayCircle } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   export let stats: ServerStatsResponseDto = {
     photos: 0,
@@ -30,15 +29,15 @@
     <p class="text-sm dark:text-immich-dark-fg">TOTAL USAGE</p>
 
     <div class="mt-5 hidden justify-between lg:flex">
-      <StatsCard logo={CameraIris} title="PHOTOS" value={stats.photos} />
-      <StatsCard logo={PlayCircle} title="VIDEOS" value={stats.videos} />
-      <StatsCard logo={Memory} title="STORAGE" value={statsUsage} unit={statsUsageUnit} />
+      <StatsCard icon={mdiCameraIris} title="PHOTOS" value={stats.photos} />
+      <StatsCard icon={mdiPlayCircle} title="VIDEOS" value={stats.videos} />
+      <StatsCard icon={mdiMemory} title="STORAGE" value={statsUsage} unit={statsUsageUnit} />
     </div>
     <div class="mt-5 flex lg:hidden">
       <div class="flex flex-col justify-between rounded-3xl bg-immich-gray p-5 dark:bg-immich-dark-gray">
         <div class="flex flex-wrap gap-x-12">
           <div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
-            <CameraIris size="25" />
+            <Icon path={mdiCameraIris} size="25" />
             <p>PHOTOS</p>
           </div>
 
@@ -50,7 +49,7 @@
         </div>
         <div class="flex flex-wrap gap-x-12">
           <div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
-            <PlayCircle size="25" />
+            <Icon path={mdiPlayCircle} size="25" />
             <p>VIDEOS</p>
           </div>
 
@@ -62,7 +61,7 @@
         </div>
         <div class="flex flex-wrap gap-x-7">
           <div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
-            <Memory size="25" />
+            <Icon path={mdiMemory} size="25" />
             <p>STORAGE</p>
           </div>
 

+ 3 - 3
web/src/lib/components/admin-page/server-stats/stats-card.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
-  import type Icon from 'svelte-material-icons/AbTesting.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
 
-  export let logo: typeof Icon;
+  export let icon: string;
   export let title: string;
   export let value: number;
   export let unit: string | undefined = undefined;
@@ -17,7 +17,7 @@
 
 <div class="flex h-[140px] w-[250px] flex-col justify-between rounded-3xl bg-immich-gray p-5 dark:bg-immich-dark-gray">
   <div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
-    <svelte:component this={logo} size="40" />
+    <Icon path={icon} size="40" />
     <p>{title}</p>
   </div>
 

+ 3 - 2
web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte

@@ -17,10 +17,11 @@
   import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
   import SettingSelect from '../setting-select.svelte';
   import SettingSwitch from '../setting-switch.svelte';
-  import HelpCircleOutline from 'svelte-material-icons/HelpCircleOutline.svelte';
   import { isEqual } from 'lodash-es';
   import { fade } from 'svelte/transition';
   import SettingAccordion from '../setting-accordion.svelte';
+  import { mdiHelpCircleOutline } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   export let ffmpegConfig: SystemConfigFFmpegDto; // this is the config that is being edited
   export let disabled = false;
@@ -93,7 +94,7 @@
       <form autocomplete="off" on:submit|preventDefault>
         <div class="ml-4 mt-4 flex flex-col gap-4">
           <p class="text-sm dark:text-immich-dark-fg">
-            <HelpCircleOutline class="inline" size="15" />
+            <Icon path={mdiHelpCircleOutline} class="inline" size="15" />
             To learn more about the terminology used here, refer to FFmpeg documentation for
             <a href="https://trac.ffmpeg.org/wiki/Encode/H.264" class="underline" target="_blank" rel="noreferrer"
               >H.264 codec</a

+ 3 - 2
web/src/lib/components/album-page/album-card.svelte

@@ -3,10 +3,11 @@
   import { locale } from '$lib/stores/preferences.store';
   import { AlbumResponseDto, api, ThumbnailFormat, UserResponseDto } from '@api';
   import { createEventDispatcher, onMount } from 'svelte';
-  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
   import IconButton from '../elements/buttons/icon-button.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import type { OnClick, OnShowContextMenu } from './album-card';
   import { getContextMenuPosition } from '../../utils/context-menu';
+  import { mdiDotsVertical } from '@mdi/js';
 
   export let album: AlbumResponseDto;
   export let isSharingView = false;
@@ -75,7 +76,7 @@
       data-testid="context-button-parent"
     >
       <IconButton color="transparent-primary">
-        <DotsVertical size="20" class="icon-white-drop-shadow" color="white" />
+        <Icon path={mdiDotsVertical} size="20" class="icon-white-drop-shadow text-white" />
       </IconButton>
     </div>
   {/if}

+ 3 - 4
web/src/lib/components/album-page/album-viewer.svelte

@@ -7,8 +7,6 @@
   import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
   import { TimeBucketSize, type AlbumResponseDto, type SharedLinkResponseDto } from '@api';
   import { onDestroy, onMount } from 'svelte';
-  import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte';
-  import FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte';
   import { dateFormats } from '../../constants';
   import { createAssetInteractionStore } from '../../stores/asset-interaction.store';
   import { AssetStore } from '../../stores/assets.store';
@@ -21,6 +19,7 @@
   import ImmichLogo from '../shared-components/immich-logo.svelte';
   import ThemeButton from '../shared-components/theme-button.svelte';
   import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
+  import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
 
   export let sharedLink: SharedLinkResponseDto;
 
@@ -122,12 +121,12 @@
           <CircleIconButton
             title="Add Photos"
             on:click={() => openFileUploadDialog(album.id)}
-            logo={FileImagePlusOutline}
+            icon={mdiFileImagePlusOutline}
           />
         {/if}
 
         {#if album.assetCount > 0 && sharedLink.allowDownload}
-          <CircleIconButton title="Download" on:click={() => downloadAlbum()} logo={FolderDownloadOutline} />
+          <CircleIconButton title="Download" on:click={() => downloadAlbum()} icon={mdiFolderDownloadOutline} />
         {/if}
 
         <ThemeButton />

+ 2 - 2
web/src/lib/components/album-page/share-info-modal.svelte

@@ -3,7 +3,6 @@
   import { AlbumResponseDto, api, UserResponseDto } from '@api';
   import BaseModal from '../shared-components/base-modal.svelte';
   import UserAvatar from '../shared-components/user-avatar.svelte';
-  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
   import MenuOption from '../shared-components/context-menu/menu-option.svelte';
@@ -11,6 +10,7 @@
   import { handleError } from '../../utils/handle-error';
   import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
   import { getContextMenuPosition } from '../../utils/context-menu';
+  import { mdiDotsVertical } from '@mdi/js';
 
   export let album: AlbumResponseDto;
 
@@ -99,7 +99,7 @@
               <div>
                 <CircleIconButton
                   on:click={(event) => showContextMenu(event, user)}
-                  logo={DotsVertical}
+                  icon={mdiDotsVertical}
                   backgroundColor="transparent"
                   hoverColor="#e2e7e9"
                   size="20"

+ 4 - 4
web/src/lib/components/album-page/user-selection-modal.svelte

@@ -3,12 +3,12 @@
   import { AlbumResponseDto, api, SharedLinkResponseDto, UserResponseDto } from '@api';
   import BaseModal from '../shared-components/base-modal.svelte';
   import UserAvatar from '../shared-components/user-avatar.svelte';
-  import Link from 'svelte-material-icons/Link.svelte';
-  import ShareCircle from 'svelte-material-icons/ShareCircle.svelte';
   import { goto } from '$app/navigation';
   import ImmichLogo from '../shared-components/immich-logo.svelte';
   import Button from '../elements/buttons/button.svelte';
   import { AppRoute } from '$lib/constants';
+  import { mdiLink, mdiShareCircle } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   export let album: AlbumResponseDto;
   let users: UserResponseDto[] = [];
@@ -128,7 +128,7 @@
       class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer"
       on:click={() => dispatch('share')}
     >
-      <Link size={24} />
+      <Icon path={mdiLink} size={24} />
       <p class="text-sm">Create link</p>
     </button>
 
@@ -137,7 +137,7 @@
         class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer"
         on:click={() => goto(AppRoute.SHARED_LINKS)}
       >
-        <ShareCircle size={24} />
+        <Icon path={mdiShareCircle} size={24} />
         <p class="text-sm">View links</p>
       </button>
     {/if}

+ 28 - 26
web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte

@@ -1,26 +1,28 @@
 <script lang="ts">
   import { page } from '$app/stores';
+  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import { photoZoomState } from '$lib/stores/zoom-image.store';
   import { clickOutside } from '$lib/utils/click-outside';
+  import { getContextMenuPosition } from '$lib/utils/context-menu';
   import { AssetJobName, AssetResponseDto, AssetTypeEnum, api } from '@api';
+  import {
+    mdiAlertOutline,
+    mdiArrowLeft,
+    mdiCloudDownloadOutline,
+    mdiContentCopy,
+    mdiDeleteOutline,
+    mdiDotsVertical,
+    mdiHeart,
+    mdiHeartOutline,
+    mdiInformationOutline,
+    mdiMagnifyMinusOutline,
+    mdiMagnifyPlusOutline,
+    mdiMotionPauseOutline,
+    mdiMoviePlayOutline,
+  } from '@mdi/js';
   import { createEventDispatcher } from 'svelte';
-  import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
-  import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
-  import AlertOutline from 'svelte-material-icons/AlertOutline.svelte';
-  import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
-  import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
-  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
-  import Heart from 'svelte-material-icons/Heart.svelte';
-  import HeartOutline from 'svelte-material-icons/HeartOutline.svelte';
-  import InformationOutline from 'svelte-material-icons/InformationOutline.svelte';
-  import MagnifyMinusOutline from 'svelte-material-icons/MagnifyMinusOutline.svelte';
-  import MagnifyPlusOutline from 'svelte-material-icons/MagnifyPlusOutline.svelte';
-  import MotionPauseOutline from 'svelte-material-icons/MotionPauseOutline.svelte';
-  import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
-  import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
   import MenuOption from '../shared-components/context-menu/menu-option.svelte';
-  import { getContextMenuPosition } from '$lib/utils/context-menu';
 
   export let asset: AssetResponseDto;
   export let showCopyButton: boolean;
@@ -74,13 +76,13 @@
   class="z-[1001] flex h-16 place-items-center justify-between bg-gradient-to-b from-black/40 px-3 transition-transform duration-200"
 >
   <div class="text-white">
-    <CircleIconButton isOpacity={true} logo={ArrowLeft} on:click={() => dispatch('goBack')} />
+    <CircleIconButton isOpacity={true} icon={mdiArrowLeft} on:click={() => dispatch('goBack')} />
   </div>
   <div class="flex w-[calc(100%-3rem)] justify-end gap-2 overflow-hidden text-white">
     {#if asset.isOffline}
       <CircleIconButton
         isOpacity={true}
-        logo={AlertOutline}
+        icon={mdiAlertOutline}
         on:click={() => dispatch('showDetail')}
         title="Asset Offline"
       />
@@ -89,14 +91,14 @@
       {#if isMotionPhotoPlaying}
         <CircleIconButton
           isOpacity={true}
-          logo={MotionPauseOutline}
+          icon={mdiMotionPauseOutline}
           title="Stop Motion Photo"
           on:click={() => dispatch('stopMotionPhoto')}
         />
       {:else}
         <CircleIconButton
           isOpacity={true}
-          logo={MotionPlayOutline}
+          icon={mdiMoviePlayOutline}
           title="Play Motion Photo"
           on:click={() => dispatch('playMotionPhoto')}
         />
@@ -106,7 +108,7 @@
       <CircleIconButton
         isOpacity={true}
         hideMobile={true}
-        logo={$photoZoomState && $photoZoomState.currentZoom > 1 ? MagnifyMinusOutline : MagnifyPlusOutline}
+        icon={$photoZoomState && $photoZoomState.currentZoom > 1 ? mdiMagnifyMinusOutline : mdiMagnifyPlusOutline}
         title="Zoom Image"
         on:click={() => {
           const zoomImage = new CustomEvent('zoomImage');
@@ -117,7 +119,7 @@
     {#if showCopyButton}
       <CircleIconButton
         isOpacity={true}
-        logo={ContentCopy}
+        icon={mdiContentCopy}
         title="Copy Image"
         on:click={() => {
           const copyEvent = new CustomEvent('copyImage');
@@ -129,7 +131,7 @@
     {#if showDownloadButton}
       <CircleIconButton
         isOpacity={true}
-        logo={CloudDownloadOutline}
+        icon={mdiCloudDownloadOutline}
         on:click={() => dispatch('download')}
         title="Download"
       />
@@ -137,7 +139,7 @@
     {#if showDetailButton}
       <CircleIconButton
         isOpacity={true}
-        logo={InformationOutline}
+        icon={mdiInformationOutline}
         on:click={() => dispatch('showDetail')}
         title="Info"
       />
@@ -145,7 +147,7 @@
     {#if isOwner}
       <CircleIconButton
         isOpacity={true}
-        logo={asset.isFavorite ? Heart : HeartOutline}
+        icon={asset.isFavorite ? mdiHeart : mdiHeartOutline}
         on:click={() => dispatch('favorite')}
         title="Favorite"
       />
@@ -153,10 +155,10 @@
 
     {#if isOwner}
       {#if !asset.isReadOnly || !asset.isExternal}
-        <CircleIconButton isOpacity={true} logo={DeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
+        <CircleIconButton isOpacity={true} icon={mdiDeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
       {/if}
       <div use:clickOutside on:outclick={() => (isShowAssetOptions = false)}>
-        <CircleIconButton isOpacity={true} logo={DotsVertical} on:click={showOptionsMenu} title="More" />
+        <CircleIconButton isOpacity={true} icon={mdiDotsVertical} on:click={showOptionsMenu} title="More" />
         {#if isShowAssetOptions}
           <ContextMenu {...contextMenuPosition} direction="left">
             {#if showSlideshow}

+ 9 - 14
web/src/lib/components/asset-viewer/asset-viewer.svelte

@@ -2,9 +2,6 @@
   import { goto } from '$app/navigation';
   import { AlbumResponseDto, api, AssetJobName, AssetResponseDto, AssetTypeEnum, SharedLinkResponseDto } from '@api';
   import { createEventDispatcher, onDestroy, onMount } from 'svelte';
-  import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
-  import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
-  import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte';
   import { fly } from 'svelte/transition';
   import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte';
   import { notificationController, NotificationType } from '../shared-components/notification/notification';
@@ -16,8 +13,6 @@
   import { ProjectionType } from '$lib/constants';
   import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
   import ProfileImageCropper from '../shared-components/profile-image-cropper.svelte';
-  import Pause from 'svelte-material-icons/Pause.svelte';
-  import Play from 'svelte-material-icons/Play.svelte';
   import { isShowDetail } from '$lib/stores/preferences.store';
   import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
   import NavigationArea from './navigation-area.svelte';
@@ -25,11 +20,11 @@
   import { handleError } from '$lib/utils/handle-error';
   import type { AssetStore } from '$lib/stores/assets.store';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
-  import Close from 'svelte-material-icons/Close.svelte';
-
   import ProgressBar, { ProgressBarStatus } from '../shared-components/progress-bar/progress-bar.svelte';
   import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
   import { featureFlags } from '$lib/stores/server-config.store';
+  import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiImageBrokenVariant, mdiPause, mdiPlay } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   export let assetStore: AssetStore | null = null;
   export let asset: AssetResponseDto;
@@ -368,14 +363,14 @@
       <!-- SlideShowController -->
       <div class="flex">
         <div class="m-4 flex gap-2">
-          <CircleIconButton logo={Close} on:click={handleStopSlideshow} title="Exit Slideshow" />
+          <CircleIconButton icon={mdiClose} on:click={handleStopSlideshow} title="Exit Slideshow" />
           <CircleIconButton
-            logo={progressBarStatus === ProgressBarStatus.Paused ? Play : Pause}
+            icon={progressBarStatus === ProgressBarStatus.Paused ? mdiPlay : mdiPause}
             on:click={() => (progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())}
             title={progressBarStatus === ProgressBarStatus.Paused ? 'Play' : 'Pause'}
           />
-          <CircleIconButton logo={ChevronLeft} on:click={navigateAssetBackward} title="Previous" />
-          <CircleIconButton logo={ChevronRight} on:click={navigateAssetForward} title="Next" />
+          <CircleIconButton icon={mdiChevronLeft} on:click={navigateAssetBackward} title="Previous" />
+          <CircleIconButton icon={mdiChevronRight} on:click={navigateAssetForward} title="Next" />
         </div>
         <ProgressBar
           autoplay
@@ -414,7 +409,7 @@
 
   {#if !isSlideshowMode && showNavigation}
     <div class="column-span-1 z-[999] col-start-1 row-span-1 row-start-2 mb-[60px] justify-self-start">
-      <NavigationArea on:click={navigateAssetBackward}><ChevronLeft size="36" /></NavigationArea>
+      <NavigationArea on:click={navigateAssetBackward}><Icon path={mdiChevronLeft} size="36" /></NavigationArea>
     </div>
   {/if}
 
@@ -425,7 +420,7 @@
           <div
             class="px-auto flex aspect-square h-full items-center justify-center bg-gray-100 dark:bg-immich-dark-gray"
           >
-            <ImageBrokenVariant size="25%" />
+            <Icon path={mdiImageBrokenVariant} size="25%" />
           </div>
         </div>
       {:else if asset.type === AssetTypeEnum.Image}
@@ -455,7 +450,7 @@
 
   {#if !isSlideshowMode && showNavigation}
     <div class="z-[999] col-span-1 col-start-4 row-span-1 row-start-2 mb-[60px] justify-self-end">
-      <NavigationArea on:click={navigateAssetForward}><ChevronRight size="36" /></NavigationArea>
+      <NavigationArea on:click={navigateAssetForward}><Icon path={mdiChevronRight} size="36" /></NavigationArea>
     </div>
   {/if}
 

+ 7 - 10
web/src/lib/components/asset-viewer/detail-panel.svelte

@@ -7,14 +7,11 @@
   import type { LatLngTuple } from 'leaflet';
   import { DateTime } from 'luxon';
   import { createEventDispatcher } from 'svelte';
-  import Calendar from 'svelte-material-icons/Calendar.svelte';
-  import CameraIris from 'svelte-material-icons/CameraIris.svelte';
-  import Close from 'svelte-material-icons/Close.svelte';
-  import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
-  import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte';
   import { asByteUnitString } from '../../utils/byte-units';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
   import UserAvatar from '../shared-components/user-avatar.svelte';
+  import { mdiCalendar, mdiCameraIris, mdiClose, mdiImageOutline, mdiMapMarkerOutline } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   export let asset: AssetResponseDto;
   export let albums: AlbumResponseDto[] = [];
@@ -91,7 +88,7 @@
       class="flex place-content-center place-items-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
       on:click={() => dispatch('close')}
     >
-      <Close size="24" />
+      <Icon path={mdiClose} size="24" />
     </button>
 
     <p class="text-lg text-immich-fg dark:text-immich-dark-fg">Info</p>
@@ -186,7 +183,7 @@
       })}
       <div class="flex gap-4 py-4">
         <div>
-          <Calendar size="24" />
+          <Icon path={mdiCalendar} size="24" />
         </div>
 
         <div>
@@ -218,7 +215,7 @@
 
     {#if asset.exifInfo?.fileSizeInByte}
       <div class="flex gap-4 py-4">
-        <div><ImageOutline size="24" /></div>
+        <div><Icon path={mdiImageOutline} size="24" /></div>
 
         <div>
           <p class="break-all">
@@ -242,7 +239,7 @@
 
     {#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.fNumber}
       <div class="flex gap-4 py-4">
-        <div><CameraIris size="24" /></div>
+        <div><Icon path={mdiCameraIris} size="24" /></div>
 
         <div>
           <p>{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}</p>
@@ -271,7 +268,7 @@
 
     {#if asset.exifInfo?.city}
       <div class="flex gap-4 py-4">
-        <div><MapMarkerOutline size="24" /></div>
+        <div><Icon path={mdiMapMarkerOutline} size="24" /></div>
 
         <div>
           <p>{asset.exifInfo.city}</p>

+ 2 - 2
web/src/lib/components/asset-viewer/download-panel.svelte

@@ -1,10 +1,10 @@
 <script lang="ts">
   import { DownloadProgress, downloadAssets, downloadManager, isDownloading } from '$lib/stores/download';
   import { locale } from '$lib/stores/preferences.store';
-  import Close from 'svelte-material-icons/Close.svelte';
   import { fly, slide } from 'svelte/transition';
   import { asByteUnitString } from '../../utils/byte-units';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
+  import { mdiClose } from '@mdi/js';
 
   const abort = (downloadKey: string, download: DownloadProgress) => {
     download.abort?.abort();
@@ -39,7 +39,7 @@
             </div>
           </div>
           <div class="absolute right-2">
-            <CircleIconButton on:click={() => abort(downloadKey, download)} size="20" logo={Close} forceDark />
+            <CircleIconButton on:click={() => abort(downloadKey, download)} size="20" icon={mdiClose} forceDark />
           </div>
         </div>
       {/each}

+ 4 - 3
web/src/lib/components/assets/thumbnail/image-thumbnail.svelte

@@ -3,7 +3,8 @@
   import { fade } from 'svelte/transition';
   import { thumbHashToDataURL } from 'thumbhash';
   import { Buffer } from 'buffer';
-  import EyeOffOutline from 'svelte-material-icons/EyeOffOutline.svelte';
+  import { mdiEyeOffOutline } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   export let url: string;
   export let altText: string;
@@ -18,7 +19,7 @@
   export let border = false;
   let complete = false;
 
-  export let eyeColor = 'white';
+  export let eyeColor: 'black' | 'white' = 'white';
 </script>
 
 <img
@@ -43,7 +44,7 @@
 
 {#if hidden}
   <div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
-    <EyeOffOutline size="2em" color={eyeColor} />
+    <Icon path={mdiEyeOffOutline} size="2em" class="text-{eyeColor}" />
   </div>
 {/if}
 

+ 19 - 16
web/src/lib/components/assets/thumbnail/thumbnail.svelte

@@ -4,16 +4,19 @@
   import { timeToSeconds } from '$lib/utils/time-to-seconds';
   import { api, AssetResponseDto, AssetTypeEnum, ThumbnailFormat } from '@api';
   import { createEventDispatcher } from 'svelte';
-  import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
-  import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
-  import Heart from 'svelte-material-icons/Heart.svelte';
-  import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte';
-  import MotionPauseOutline from 'svelte-material-icons/MotionPauseOutline.svelte';
-  import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
   import { fade } from 'svelte/transition';
   import ImageThumbnail from './image-thumbnail.svelte';
   import VideoThumbnail from './video-thumbnail.svelte';
-  import Rotate360Icon from 'svelte-material-icons/Rotate360.svelte';
+  import {
+    mdiArchiveArrowDownOutline,
+    mdiCheckCircle,
+    mdiHeart,
+    mdiImageBrokenVariant,
+    mdiMotionPauseOutline,
+    mdiMotionPlayOutline,
+    mdiRotate360,
+  } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   const dispatch = createEventDispatcher();
 
@@ -93,13 +96,13 @@
             {disabled}
           >
             {#if disabled}
-              <CheckCircle size="24" class="text-zinc-800" />
+              <Icon path={mdiCheckCircle} size="24" class="text-zinc-800" />
             {:else if selected}
               <div class="rounded-full bg-[#D9DCEF] dark:bg-[#232932]">
-                <CheckCircle size="24" class="text-immich-primary" />
+                <Icon path={mdiCheckCircle} size="24" class="text-immich-primary" />
               </div>
             {:else}
-              <CheckCircle size="24" class="text-white/80 hover:text-white" />
+              <Icon path={mdiCheckCircle} size="24" class="text-white/80 hover:text-white" />
             {/if}
           </button>
         {/if}
@@ -119,20 +122,20 @@
         <!-- Favorite asset star -->
         {#if !api.isSharedLink && asset.isFavorite}
           <div class="absolute bottom-2 left-2 z-10">
-            <Heart size="24" class="text-white" />
+            <Icon path={mdiHeart} size="24" class="text-white" />
           </div>
         {/if}
 
         {#if !api.isSharedLink && showArchiveIcon && asset.isArchived}
           <div class="absolute {asset.isFavorite ? 'bottom-10' : 'bottom-2'} left-2 z-10">
-            <ArchiveArrowDownOutline size="24" class="text-white" />
+            <Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />
           </div>
         {/if}
 
         {#if asset.type === AssetTypeEnum.Image && asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR}
           <div class="absolute right-0 top-0 z-20 flex place-items-center gap-1 text-xs font-medium text-white">
             <span class="pr-2 pt-2">
-              <Rotate360Icon size="24" />
+              <Icon path={mdiRotate360} size="24" />
             </span>
           </div>
         {/if}
@@ -148,7 +151,7 @@
           />
         {:else}
           <div class="flex h-full w-full items-center justify-center p-4">
-            <ImageBrokenVariant size="48" />
+            <Icon path={mdiImageBrokenVariant} size="48" />
           </div>
         {/if}
 
@@ -167,8 +170,8 @@
           <div class="absolute top-0 h-full w-full">
             <VideoThumbnail
               url={api.getAssetFileUrl(asset.livePhotoVideoId, false, true)}
-              pauseIcon={MotionPauseOutline}
-              playIcon={MotionPlayOutline}
+              pauseIcon={mdiMotionPauseOutline}
+              playIcon={mdiMotionPlayOutline}
               showTime={false}
               curve={selected}
               playbackOnIconHover

+ 7 - 8
web/src/lib/components/assets/thumbnail/video-thumbnail.svelte

@@ -1,9 +1,8 @@
 <script lang="ts">
   import { Duration } from 'luxon';
-  import PauseCircleOutline from 'svelte-material-icons/PauseCircleOutline.svelte';
-  import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
-  import AlertCircleOutline from 'svelte-material-icons/AlertCircleOutline.svelte';
   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
+  import { mdiAlertCircleOutline, mdiPauseCircleOutline, mdiPlayCircleOutline } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   export let url: string;
   export let durationInSeconds = 0;
@@ -11,8 +10,8 @@
   export let playbackOnIconHover = false;
   export let showTime = true;
   export let curve = false;
-  export let playIcon = PlayCircleOutline;
-  export let pauseIcon = PauseCircleOutline;
+  export let playIcon = mdiPlayCircleOutline;
+  export let pauseIcon = mdiPauseCircleOutline;
 
   let remainingSeconds = durationInSeconds;
   let loading = true;
@@ -55,12 +54,12 @@
       {#if loading}
         <LoadingSpinner />
       {:else if error}
-        <AlertCircleOutline size="24" class="text-red-600" />
+        <Icon path={mdiAlertCircleOutline} size="24" class="text-red-600" />
       {:else}
-        <svelte:component this={pauseIcon} size="24" />
+        <Icon path={pauseIcon} size="24" />
       {/if}
     {:else}
-      <svelte:component this={playIcon} size="24" />
+      <Icon path={playIcon} size="24" />
     {/if}
   </span>
 </div>

+ 3 - 3
web/src/lib/components/elements/buttons/circle-icon-button.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
-  import type Icon from 'svelte-material-icons/AbTesting.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
 
-  export let logo: typeof Icon;
+  export let icon: string;
   export let backgroundColor = '';
   export let hoverColor = '#e2e7e9';
   export let padding = '3';
@@ -23,7 +23,7 @@
   {hideMobile && 'hidden sm:flex'}"
   on:click
 >
-  <svelte:component this={logo} {size} />
+  <Icon path={icon} {size} />
   <slot />
 </button>
 

+ 7 - 5
web/src/lib/components/elements/dropdown.svelte

@@ -5,12 +5,14 @@
 </script>
 
 <script lang="ts" generics="T">
+  import Icon from './icon.svelte';
+
+  import { mdiCheck } from '@mdi/js';
+
   import _ from 'lodash';
   import LinkButton from './buttons/link-button.svelte';
   import { clickOutside } from '$lib/utils/click-outside';
   import { fly } from 'svelte/transition';
-  import type Icon from 'svelte-material-icons/DotsVertical.svelte';
-  import Check from 'svelte-material-icons/Check.svelte';
   import { createEventDispatcher } from 'svelte';
 
   const dispatch = createEventDispatcher<{
@@ -24,7 +26,7 @@
 
   type RenderedOption = {
     title: string;
-    icon?: typeof Icon;
+    icon?: string;
   };
 
   let showMenu = false;
@@ -61,7 +63,7 @@
   <LinkButton on:click={() => (showMenu = true)}>
     <div class="flex place-items-center gap-2 text-sm">
       {#if renderedSelectedOption?.icon}
-        <svelte:component this={renderedSelectedOption.icon} size="18" />
+        <Icon path={renderedSelectedOption.icon} size="18" />
       {/if}
       <p class="hidden sm:block">{renderedSelectedOption.title}</p>
     </div>
@@ -81,7 +83,7 @@
         >
           {#if _.isEqual(selectedOption, option)}
             <div class="text-immich-primary dark:text-immich-dark-primary">
-              <Check size="18" />
+              <Icon path={mdiCheck} size="18" />
             </div>
             <p class="justify-self-start text-immich-primary dark:text-immich-dark-primary">
               {renderedOption.title}

+ 36 - 0
web/src/lib/components/elements/icon.svelte

@@ -0,0 +1,36 @@
+<script lang="ts">
+  import type { AriaRole } from 'svelte/elements';
+
+  export let size: string | number = '1em';
+  export let color = 'currentColor';
+  export let path: string;
+  export let title = '';
+  export let desc = '';
+  export let flipped = false;
+  let className = '';
+  export { className as class };
+  export let viewBox = '0 0 24 24';
+  export let role: AriaRole = 'img';
+  export let ariaHidden: boolean | undefined = undefined;
+  export let ariaLabel: string | undefined = undefined;
+  export let ariaLabelledby: string | undefined = undefined;
+</script>
+
+<svg
+  width={size}
+  height={size}
+  {viewBox}
+  class="{className} {flipped && '-scale-x-100'}"
+  {role}
+  aria-label={ariaLabel}
+  aria-hidden={ariaHidden}
+  aria-labelledby={ariaLabelledby}
+>
+  {#if title}
+    <title>{title}</title>
+  {/if}
+  {#if desc}
+    <desc>{desc}</desc>
+  {/if}
+  <path d={path} fill={color} />
+</svg>

+ 5 - 6
web/src/lib/components/faces-page/merge-face-selector.svelte

@@ -6,15 +6,14 @@
   import { fly } from 'svelte/transition';
   import ControlAppBar from '../shared-components/control-app-bar.svelte';
   import Button from '../elements/buttons/button.svelte';
-  import Merge from 'svelte-material-icons/Merge.svelte';
-  import CallMerge from 'svelte-material-icons/CallMerge.svelte';
   import { flip } from 'svelte/animate';
   import { NotificationType, notificationController } from '../shared-components/notification/notification';
   import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
   import { handleError } from '$lib/utils/handle-error';
   import { goto, invalidateAll } from '$app/navigation';
   import { AppRoute } from '$lib/constants';
-  import SwapHorizontal from 'svelte-material-icons/SwapHorizontal.svelte';
+  import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   export let person: PersonResponseDto;
   let people: PersonResponseDto[] = [];
@@ -104,7 +103,7 @@
           isShowConfirmation = true;
         }}
       >
-        <Merge size={18} />
+        <Icon path={mdiMerge} size={18} />
         <span class="ml-2"> Merge</span></Button
       >
     </svelte:fragment>
@@ -123,10 +122,10 @@
 
           {#if hasSelection}
             <span class="grid grid-cols-1"
-              ><CallMerge size={48} class="rotate-90 dark:text-white" />
+              ><Icon path={mdiCallMerge} size={48} class="rotate-90 dark:text-white" />
               {#if selectedPeople.length === 1}
                 <button class="flex justify-center" on:click={handleSwapPeople}
-                  ><SwapHorizontal size={24} class="dark:text-white" />
+                  ><Icon path={mdiSwapHorizontal} size={24} class="dark:text-white" />
                 </button>
               {/if}
             </span>

+ 5 - 6
web/src/lib/components/faces-page/merge-suggestion-modal.svelte

@@ -2,12 +2,11 @@
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import { api, type PersonResponseDto } from '@api';
   import { createEventDispatcher } from 'svelte';
-  import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
-  import Close from 'svelte-material-icons/Close.svelte';
-  import Merge from 'svelte-material-icons/Merge.svelte';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
   import Button from '../elements/buttons/button.svelte';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
+  import { mdiArrowLeft, mdiClose, mdiMerge } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   const dispatch = createEventDispatcher<{
     reject: void;
@@ -40,7 +39,7 @@
           Merge faces - {title}
         </h1>
         <div class="p-2">
-          <CircleIconButton logo={Close} on:click={() => dispatch('close')} />
+          <CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
         </div>
       </div>
 
@@ -57,7 +56,7 @@
           </div>
           <div class="mx-0.5 flex md:mx-2">
             <CircleIconButton
-              logo={Merge}
+              icon={mdiMerge}
               on:click={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
             />
           </div>
@@ -83,7 +82,7 @@
         {:else}
           <div class="grid w-full grid-cols-1 gap-2">
             <div class="px-2">
-              <button on:click={() => (choosePersonToMerge = false)}> <ArrowLeft /></button>
+              <button on:click={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
             </div>
             <div class="flex items-center justify-center">
               <div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">

+ 3 - 2
web/src/lib/components/faces-page/people-card.svelte

@@ -3,12 +3,13 @@
   import { getContextMenuPosition } from '$lib/utils/context-menu';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
   import IconButton from '../elements/buttons/icon-button.svelte';
-  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
   import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
   import MenuOption from '../shared-components/context-menu/menu-option.svelte';
   import Portal from '../shared-components/portal/portal.svelte';
   import { createEventDispatcher } from 'svelte';
   import { AppRoute } from '$lib/constants';
+  import { mdiDotsVertical } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   export let person: PersonResponseDto;
 
@@ -71,7 +72,7 @@
     id={`icon-${person.id}`}
   >
     <IconButton color="transparent-primary">
-      <DotsVertical size="20" class="icon-white-drop-shadow" color="white" />
+      <Icon path={mdiDotsVertical} size="20" class="icon-white-drop-shadow text-white" />
     </IconButton>
   </button>
 </div>

+ 3 - 2
web/src/lib/components/faces-page/set-birth-date-modal.svelte

@@ -1,8 +1,9 @@
 <script lang="ts">
   import { createEventDispatcher } from 'svelte';
-  import Cake from 'svelte-material-icons/Cake.svelte';
   import Button from '../elements/buttons/button.svelte';
   import FullScreenModal from '../shared-components/full-screen-modal.svelte';
+  import { mdiCake } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   export let birthDate: string;
 
@@ -22,7 +23,7 @@
     <div
       class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
     >
-      <Cake size="4em" />
+      <Icon path={mdiCake} size="4em" />
       <h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Set date of birth</h1>
 
       <p class="text-sm dark:text-immich-dark-fg">

+ 8 - 7
web/src/lib/components/faces-page/show-hide.svelte

@@ -2,13 +2,10 @@
   import { fly } from 'svelte/transition';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import { quintOut } from 'svelte/easing';
-  import Close from 'svelte-material-icons/Close.svelte';
   import IconButton from '../elements/buttons/icon-button.svelte';
   import { createEventDispatcher } from 'svelte';
   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
-  import Restart from 'svelte-material-icons/Restart.svelte';
-  import Eye from 'svelte-material-icons/Eye.svelte';
-  import EyeOff from 'svelte-material-icons/EyeOff.svelte';
+  import { mdiClose, mdiEye, mdiEyeOff, mdiRestart } from '@mdi/js';
 
   const dispatch = createEventDispatcher();
 
@@ -24,15 +21,19 @@
     class="sticky top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
   >
     <div class="flex items-center">
-      <CircleIconButton logo={Close} on:click={() => dispatch('closeClick')} />
+      <CircleIconButton icon={mdiClose} on:click={() => dispatch('closeClick')} />
       <p class="ml-4 hidden sm:block">Show & hide faces</p>
     </div>
     <div class="flex items-center justify-end">
       <div class="flex items-center md:mr-8">
-        <CircleIconButton title="Reset faces visibility" logo={Restart} on:click={() => dispatch('reset-visibility')} />
+        <CircleIconButton
+          title="Reset faces visibility"
+          icon={mdiRestart}
+          on:click={() => dispatch('reset-visibility')}
+        />
         <CircleIconButton
           title="Toggle visibility"
-          logo={toggleVisibility ? Eye : EyeOff}
+          icon={toggleVisibility ? mdiEye : mdiEyeOff}
           on:click={() => dispatch('toggle-visibility')}
         />
       </div>

+ 3 - 2
web/src/lib/components/forms/api-key-form.svelte

@@ -1,9 +1,10 @@
 <script lang="ts">
   import type { APIKeyResponseDto } from '@api';
   import { createEventDispatcher } from 'svelte';
-  import KeyVariant from 'svelte-material-icons/KeyVariant.svelte';
   import Button from '../elements/buttons/button.svelte';
   import FullScreenModal from '../shared-components/full-screen-modal.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
+  import { mdiKeyVariant } from '@mdi/js';
 
   export let apiKey: Partial<APIKeyResponseDto>;
   export let title = 'API Key';
@@ -22,7 +23,7 @@
     <div
       class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
     >
-      <KeyVariant size="4em" />
+      <Icon path={mdiKeyVariant} size="4em" />
       <h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
         {title}
       </h1>

+ 3 - 2
web/src/lib/components/forms/api-key-secret.svelte

@@ -1,9 +1,10 @@
 <script lang="ts">
   import { createEventDispatcher, onMount } from 'svelte';
-  import KeyVariant from 'svelte-material-icons/KeyVariant.svelte';
   import { copyToClipboard } from '@api';
   import Button from '../elements/buttons/button.svelte';
   import FullScreenModal from '../shared-components/full-screen-modal.svelte';
+  import { mdiKeyVariant } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   export let secret = '';
 
@@ -24,7 +25,7 @@
     <div
       class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
     >
-      <KeyVariant size="4em" />
+      <Icon path={mdiKeyVariant} size="4em" />
       <h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">API Key</h1>
 
       <p class="text-sm dark:text-immich-dark-fg">

+ 3 - 4
web/src/lib/components/forms/edit-album-form.svelte

@@ -1,11 +1,10 @@
 <script lang="ts">
   import { AlbumResponseDto, api } from '@api';
   import { createEventDispatcher } from 'svelte';
-  import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
-
+  import Icon from '$lib/components/elements/icon.svelte';
   import Button from '../elements/buttons/button.svelte';
-
   import { handleError } from '../../utils/handle-error';
+  import { mdiImageAlbum } from '@mdi/js';
 
   export let album: AlbumResponseDto;
 
@@ -36,7 +35,7 @@
   <div
     class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
   >
-    <ImageAlbum size="4em" />
+    <Icon path={mdiImageAlbum} size="4em" />
     <h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Edit album</h1>
   </div>
 

+ 3 - 2
web/src/lib/components/forms/edit-user-form.svelte

@@ -1,11 +1,12 @@
 <script lang="ts">
   import { api, UserResponseDto } from '@api';
   import { createEventDispatcher } from 'svelte';
-  import AccountEditOutline from 'svelte-material-icons/AccountEditOutline.svelte';
   import { notificationController, NotificationType } from '../shared-components/notification/notification';
   import Button from '../elements/buttons/button.svelte';
   import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
   import { handleError } from '../../utils/handle-error';
+  import Icon from '$lib/components/elements/icon.svelte';
+  import { mdiAccountEditOutline } from '@mdi/js';
 
   export let user: UserResponseDto;
   export let canResetPassword = true;
@@ -72,7 +73,7 @@
   <div
     class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
   >
-    <AccountEditOutline size="4em" />
+    <Icon path={mdiAccountEditOutline} size="4em" />
     <h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Edit user</h1>
   </div>
 

+ 3 - 2
web/src/lib/components/forms/library-exclusion-pattern-form.svelte

@@ -1,8 +1,9 @@
 <script lang="ts">
   import { createEventDispatcher } from 'svelte';
-  import FolderRemove from 'svelte-material-icons/FolderRemove.svelte';
   import Button from '../elements/buttons/button.svelte';
   import FullScreenModal from '../shared-components/full-screen-modal.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
+  import { mdiFolderRemove } from '@mdi/js';
 
   export let exclusionPattern: string;
   export let canDelete = false;
@@ -20,7 +21,7 @@
     <div
       class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
     >
-      <FolderRemove size="4em" />
+      <Icon path={mdiFolderRemove} size="4em" />
       <h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Add Exclusion pattern</h1>
     </div>
 

+ 3 - 2
web/src/lib/components/forms/library-import-path-form.svelte

@@ -1,8 +1,9 @@
 <script lang="ts">
   import { createEventDispatcher } from 'svelte';
-  import FolderSync from 'svelte-material-icons/FolderSync.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import Button from '../elements/buttons/button.svelte';
   import FullScreenModal from '../shared-components/full-screen-modal.svelte';
+  import { mdiFolderSync } from '@mdi/js';
 
   export let importPath: string;
   export let title = 'Import path';
@@ -22,7 +23,7 @@
     <div
       class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
     >
-      <FolderSync size="4em" />
+      <Icon path={mdiFolderSync} size="4em" />
       <h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
         {title}
       </h1>

+ 3 - 2
web/src/lib/components/forms/library-import-paths-form.svelte

@@ -4,8 +4,9 @@
   import { handleError } from '../../utils/handle-error';
   import LibraryImportPathForm from './library-import-path-form.svelte';
   import { onMount } from 'svelte';
-  import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import type { LibraryResponseDto } from '@api';
+  import { mdiPencilOutline } from '@mdi/js';
 
   export let library: Partial<LibraryResponseDto>;
 
@@ -141,7 +142,7 @@
               }}
               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"
             >
-              <PencilOutline size="16" />
+              <Icon path={mdiPencilOutline} size="16" />
             </button>
           </td>
         </tr>

+ 3 - 2
web/src/lib/components/forms/library-scan-settings-form.svelte

@@ -4,8 +4,9 @@
   import { LibraryType, type LibraryResponseDto } from '@api';
   import { handleError } from '../../utils/handle-error';
   import { onMount } from 'svelte';
-  import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import LibraryExclusionPatternForm from './library-exclusion-pattern-form.svelte';
+  import { mdiPencilOutline } from '@mdi/js';
 
   export let library: Partial<LibraryResponseDto>;
 
@@ -139,7 +140,7 @@
               }}
               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"
             >
-              <PencilOutline size="16" />
+              <Icon path={mdiPencilOutline} size="16" />
             </button>
           </td>
         </tr>

+ 6 - 11
web/src/lib/components/memory-page/memory-viewer.svelte

@@ -6,12 +6,6 @@
   import { goto } from '$app/navigation';
   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
   import { fromLocalDateTime } from '$lib/utils/timeline-util';
-  import Play from 'svelte-material-icons/Play.svelte';
-  import Pause from 'svelte-material-icons/Pause.svelte';
-  import ChevronDown from 'svelte-material-icons/ChevronDown.svelte';
-  import ChevronUp from 'svelte-material-icons/ChevronUp.svelte';
-  import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
-  import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
   import { AppRoute } from '$lib/constants';
   import { page } from '$app/stores';
   import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
@@ -20,6 +14,7 @@
   import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
   import { fade } from 'svelte/transition';
   import { tweened } from 'svelte/motion';
+  import { mdiChevronDown, mdiChevronLeft, mdiChevronRight, mdiChevronUp, mdiPause, mdiPlay } from '@mdi/js';
 
   const parseIndex = (s: string | null, max: number | null) => Math.max(Math.min(parseInt(s ?? '') || 0, max ?? 0), 0);
 
@@ -115,7 +110,7 @@
 
       {#if !galleryInView}
         <div class="flex place-content-center place-items-center gap-2 overflow-hidden">
-          <CircleIconButton logo={paused ? Play : Pause} forceDark on:click={() => (paused = !paused)} />
+          <CircleIconButton icon={paused ? mdiPlay : mdiPause} forceDark on:click={() => (paused = !paused)} />
 
           {#each currentMemory.assets as _, i}
             <button class="relative w-full py-2" on:click={() => goto(`?memory=${memoryIndex}&asset=${i}`)}>
@@ -147,7 +142,7 @@
         class:opacity-100={galleryInView}
       >
         <button on:click={() => memoryWrapper.scrollIntoView({ behavior: 'smooth' })} disabled={!galleryInView}>
-          <CircleIconButton logo={ChevronUp} backgroundColor="white" forceDark />
+          <CircleIconButton icon={mdiChevronUp} backgroundColor="white" forceDark />
         </button>
       </div>
     {/if}
@@ -190,14 +185,14 @@
               <div class="ml-4 flex h-full flex-col place-content-center place-items-center">
                 <div class="inline-block">
                   {#if canGoBack}
-                    <CircleIconButton logo={ChevronLeft} backgroundColor="#202123" on:click={toPrevious} />
+                    <CircleIconButton icon={mdiChevronLeft} backgroundColor="#202123" on:click={toPrevious} />
                   {/if}
                 </div>
               </div>
               <div class="mr-4 flex h-full flex-col place-content-center place-items-center">
                 <div class="inline-block">
                   {#if canGoForward}
-                    <CircleIconButton logo={ChevronRight} backgroundColor="#202123" on:click={toNext} />
+                    <CircleIconButton icon={mdiChevronRight} backgroundColor="#202123" on:click={toNext} />
                   {/if}
                 </div>
               </div>
@@ -260,7 +255,7 @@
         class:opacity-100={!galleryInView}
       >
         <button on:click={() => memoryGallery.scrollIntoView({ behavior: 'smooth' })}>
-          <CircleIconButton logo={ChevronDown} backgroundColor="white" forceDark />
+          <CircleIconButton icon={mdiChevronDown} backgroundColor="white" forceDark />
         </button>
       </div>
 

+ 4 - 6
web/src/lib/components/photos-page/actions/archive-action.svelte

@@ -6,11 +6,9 @@
   } from '$lib/components/shared-components/notification/notification';
   import { handleError } from '$lib/utils/handle-error';
   import { api } from '@api';
-  import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
-  import ArchiveArrowUpOutline from 'svelte-material-icons/ArchiveArrowUpOutline.svelte';
-  import TimerSand from 'svelte-material-icons/TimerSand.svelte';
   import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
   import { OnArchive, getAssetControlContext } from '../asset-select-control-bar.svelte';
+  import { mdiArchiveArrowUpOutline, mdiArchiveArrowDownOutline, mdiTimerSand } from '@mdi/js';
 
   export let onArchive: OnArchive | undefined = undefined;
 
@@ -18,7 +16,7 @@
   export let unarchive = false;
 
   $: text = unarchive ? 'Unarchive' : 'Archive';
-  $: logo = unarchive ? ArchiveArrowUpOutline : ArchiveArrowDownOutline;
+  $: icon = unarchive ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline;
 
   let loading = false;
 
@@ -62,8 +60,8 @@
 
 {#if !menuItem}
   {#if loading}
-    <CircleIconButton title="Loading" logo={TimerSand} />
+    <CircleIconButton title="Loading" icon={mdiTimerSand} />
   {:else}
-    <CircleIconButton title={text} {logo} on:click={handleArchive} />
+    <CircleIconButton title={text} {icon} on:click={handleArchive} />
   {/if}
 {/if}

+ 3 - 3
web/src/lib/components/photos-page/actions/create-shared-link.svelte

@@ -1,9 +1,9 @@
 <script lang="ts">
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
-  import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
-  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
+  import { mdiShareVariantOutline } from '@mdi/js';
   import { createEventDispatcher } from 'svelte';
+  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 
   let showModal = false;
   const dispatch = createEventDispatcher();
@@ -14,7 +14,7 @@
   };
 </script>
 
-<CircleIconButton title="Share" logo={ShareVariantOutline} on:click={() => (showModal = true)} />
+<CircleIconButton title="Share" icon={mdiShareVariantOutline} on:click={() => (showModal = true)} />
 
 {#if showModal}
   <CreateSharedLinkModal

+ 3 - 5
web/src/lib/components/photos-page/actions/delete-assets.svelte

@@ -7,13 +7,11 @@
   } from '$lib/components/shared-components/notification/notification';
   import { handleError } from '$lib/utils/handle-error';
   import { api } from '@api';
-  import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
-  import TimerSand from 'svelte-material-icons/TimerSand.svelte';
-
   import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
   import { OnAssetDelete, getAssetControlContext } from '../asset-select-control-bar.svelte';
   import { createEventDispatcher } from 'svelte';
   import { featureFlags } from '$lib/stores/server-config.store';
+  import { mdiTimerSand, mdiDeleteOutline } from '@mdi/js';
 
   export let onAssetDelete: OnAssetDelete;
   export let menuItem = false;
@@ -70,9 +68,9 @@
 {#if menuItem}
   <MenuOption text={force ? 'Permanently Delete' : 'Delete'} on:click={handleTrash} />
 {:else if loading}
-  <CircleIconButton title="Loading" logo={TimerSand} />
+  <CircleIconButton title="Loading" icon={mdiTimerSand} />
 {:else}
-  <CircleIconButton title="Delete" logo={DeleteOutline} on:click={handleTrash} />
+  <CircleIconButton title="Delete" icon={mdiDeleteOutline} on:click={handleTrash} />
 {/if}
 
 {#if isShowConfirmation}

+ 2 - 2
web/src/lib/components/photos-page/actions/download-action.svelte

@@ -1,9 +1,9 @@
 <script lang="ts">
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import { downloadArchive, downloadFile } from '$lib/utils/asset-utils';
-  import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
   import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
   import { getAssetControlContext } from '../asset-select-control-bar.svelte';
+  import { mdiCloudDownloadOutline } from '@mdi/js';
 
   export let filename = 'immich.zip';
   export let menuItem = false;
@@ -26,5 +26,5 @@
 {#if menuItem}
   <MenuOption text="Download" on:click={handleDownloadFiles} />
 {:else}
-  <CircleIconButton title="Download" logo={CloudDownloadOutline} on:click={handleDownloadFiles} />
+  <CircleIconButton title="Download" icon={mdiCloudDownloadOutline} on:click={handleDownloadFiles} />
 {/if}

+ 4 - 6
web/src/lib/components/photos-page/actions/favorite-action.svelte

@@ -7,10 +7,8 @@
   } from '$lib/components/shared-components/notification/notification';
   import { handleError } from '$lib/utils/handle-error';
   import { api } from '@api';
-  import HeartMinusOutline from 'svelte-material-icons/HeartMinusOutline.svelte';
-  import HeartOutline from 'svelte-material-icons/HeartOutline.svelte';
-  import TimerSand from 'svelte-material-icons/TimerSand.svelte';
   import { OnFavorite, getAssetControlContext } from '../asset-select-control-bar.svelte';
+  import { mdiHeartMinusOutline, mdiHeartOutline, mdiTimerSand } from '@mdi/js';
 
   export let onFavorite: OnFavorite | undefined = undefined;
 
@@ -18,7 +16,7 @@
   export let removeFavorite: boolean;
 
   $: text = removeFavorite ? 'Remove from Favorites' : 'Favorite';
-  $: logo = removeFavorite ? HeartMinusOutline : HeartOutline;
+  $: icon = removeFavorite ? mdiHeartMinusOutline : mdiHeartOutline;
 
   let loading = false;
 
@@ -62,8 +60,8 @@
 
 {#if !menuItem}
   {#if loading}
-    <CircleIconButton title="Loading" logo={TimerSand} />
+    <CircleIconButton title="Loading" icon={mdiTimerSand} />
   {:else}
-    <CircleIconButton title={text} {logo} on:click={handleFavorite} />
+    <CircleIconButton title={text} {icon} on:click={handleFavorite} />
   {/if}
 {/if}

+ 2 - 2
web/src/lib/components/photos-page/actions/remove-from-album.svelte

@@ -6,9 +6,9 @@
     notificationController,
   } from '$lib/components/shared-components/notification/notification';
   import { AlbumResponseDto, api } from '@api';
-  import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
   import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
   import { getAssetControlContext } from '../asset-select-control-bar.svelte';
+  import { mdiDeleteOutline } from '@mdi/js';
 
   export let album: AlbumResponseDto;
   export let onRemove: ((assetIds: string[]) => void) | undefined = undefined;
@@ -53,7 +53,7 @@
 {#if menuItem}
   <MenuOption text="Remove from album" on:click={() => (isShowConfirmation = true)} />
 {:else}
-  <CircleIconButton title="Remove from album" logo={DeleteOutline} on:click={() => (isShowConfirmation = true)} />
+  <CircleIconButton title="Remove from album" icon={mdiDeleteOutline} on:click={() => (isShowConfirmation = true)} />
 {/if}
 
 {#if isShowConfirmation}

+ 2 - 2
web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte

@@ -1,11 +1,11 @@
 <script lang="ts">
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import { SharedLinkResponseDto, api } from '@api';
-  import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
   import ConfirmDialogue from '../../shared-components/confirm-dialogue.svelte';
   import { getAssetControlContext } from '../asset-select-control-bar.svelte';
   import { NotificationType, notificationController } from '../../shared-components/notification/notification';
   import { handleError } from '../../../utils/handle-error';
+  import { mdiDeleteOutline } from '@mdi/js';
 
   export let sharedLink: SharedLinkResponseDto;
 
@@ -45,7 +45,7 @@
   };
 </script>
 
-<CircleIconButton title="Remove from shared link" on:click={() => (removing = true)} logo={DeleteOutline} />
+<CircleIconButton title="Remove from shared link" on:click={() => (removing = true)} icon={mdiDeleteOutline} />
 
 {#if removing}
   <ConfirmDialogue

+ 3 - 2
web/src/lib/components/photos-page/actions/restore-assets.svelte

@@ -5,9 +5,10 @@
   } from '$lib/components/shared-components/notification/notification';
   import { handleError } from '$lib/utils/handle-error';
   import { api } from '@api';
-  import History from 'svelte-material-icons/History.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import Button from '../../elements/buttons/button.svelte';
   import { OnRestore, getAssetControlContext } from '../asset-select-control-bar.svelte';
+  import { mdiHistory } from '@mdi/js';
 
   export let onRestore: OnRestore | undefined = undefined;
 
@@ -38,6 +39,6 @@
 </script>
 
 <Button disabled={loading} size="sm" color="transparent-gray" shadow={false} rounded="lg" on:click={handleRestore}>
-  <History size="24" />
+  <Icon path={mdiHistory} size="24" />
   <span class="ml-2">Restore</span>
 </Button>

+ 3 - 4
web/src/lib/components/photos-page/actions/select-all-assets.svelte

@@ -3,9 +3,8 @@
   import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { BucketPosition, type AssetStore } from '$lib/stores/assets.store';
   import { handleError } from '$lib/utils/handle-error';
-  import SelectAll from 'svelte-material-icons/SelectAll.svelte';
-  import TimerSand from 'svelte-material-icons/TimerSand.svelte';
   import { get } from 'svelte/store';
+  import { mdiTimerSand, mdiSelectAll } from '@mdi/js';
 
   export let assetStore: AssetStore;
   export let assetInteractionStore: AssetInteractionStore;
@@ -32,8 +31,8 @@
 </script>
 
 {#if selecting}
-  <CircleIconButton title="Delete" logo={TimerSand} />
+  <CircleIconButton title="Delete" icon={mdiTimerSand} />
 {/if}
 {#if !selecting}
-  <CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
+  <CircleIconButton title="Select all" icon={mdiSelectAll} on:click={handleSelectAll} />
 {/if}

+ 4 - 4
web/src/lib/components/photos-page/asset-date-group.svelte

@@ -5,14 +5,14 @@
   import type { AssetResponseDto } from '@api';
   import justifiedLayout from 'justified-layout';
   import { createEventDispatcher } from 'svelte';
-  import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
-  import CircleOutline from 'svelte-material-icons/CircleOutline.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import { fly } from 'svelte/transition';
   import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import type { AssetStore } from '$lib/stores/assets.store';
   import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import type { Viewport } from '$lib/stores/assets.store';
+  import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
 
   export let assets: AssetResponseDto[];
   export let bucketDate: string;
@@ -154,9 +154,9 @@
             on:keydown={() => handleSelectGroup(groupTitle, groupAssets)}
           >
             {#if $selectedGroup.has(groupTitle)}
-              <CheckCircle size="24" color="#4250af" />
+              <Icon path={mdiCheckCircle} size="24" color="#4250af" />
             {:else}
-              <CircleOutline size="24" color="#757575" />
+              <Icon path={mdiCircleOutline} size="24" color="#757575" />
             {/if}
           </div>
         {/if}

+ 2 - 3
web/src/lib/components/photos-page/asset-select-context-menu.svelte

@@ -9,10 +9,9 @@
 <script lang="ts">
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
-  import type Icon from 'svelte-material-icons/AbTesting.svelte';
   import { getContextMenuPosition } from '$lib/utils/context-menu';
 
-  export let icon: typeof Icon;
+  export let icon: string;
   export let title: string;
 
   let showContextMenu = false;
@@ -27,7 +26,7 @@
 </script>
 
 <div use:clickOutside on:outclick={() => (showContextMenu = false)}>
-  <CircleIconButton {title} logo={icon} on:click={handleShowMenu} />
+  <CircleIconButton {title} {icon} on:click={handleShowMenu} />
   {#if showContextMenu}
     <ContextMenu {...contextMenuPosition}>
       <div class="flex flex-col rounded-lg">

+ 2 - 2
web/src/lib/components/photos-page/asset-select-control-bar.svelte

@@ -19,8 +19,8 @@
 <script lang="ts">
   import { locale } from '$lib/stores/preferences.store';
   import type { AssetResponseDto } from '@api';
-  import Close from 'svelte-material-icons/Close.svelte';
   import ControlAppBar from '../shared-components/control-app-bar.svelte';
+  import { mdiClose } from '@mdi/js';
 
   export let assets: Set<AssetResponseDto>;
   export let clearSelect: () => void;
@@ -28,7 +28,7 @@
   setContext({ getAssets: () => assets, clearSelect });
 </script>
 
-<ControlAppBar on:close-button-click={clearSelect} backIcon={Close} tailwindClasses="bg-white shadow-md">
+<ControlAppBar on:close-button-click={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
   <p class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
     Selected {assets.size.toLocaleString($locale)}
   </p>

+ 4 - 4
web/src/lib/components/photos-page/memory-lane.svelte

@@ -1,11 +1,11 @@
 <script lang="ts">
   import { onMount } from 'svelte';
   import { api } from '@api';
-  import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
-  import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import { memoryStore } from '$lib/stores/memory.store';
   import { goto } from '$app/navigation';
   import { fade } from 'svelte/transition';
+  import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
 
   $: shouldRender = $memoryStore?.length > 0;
 
@@ -50,7 +50,7 @@
               class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
               on:click={scrollLeft}
             >
-              <ChevronLeft size="36" /></button
+              <Icon path={mdiChevronLeft} size="36" /></button
             >
           </div>
         {/if}
@@ -60,7 +60,7 @@
               class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
               on:click={scrollRight}
             >
-              <ChevronRight size="36" /></button
+              <Icon path={mdiChevronRight} size="36" /></button
             >
           </div>
         {/if}

+ 5 - 8
web/src/lib/components/share-page/individual-shared-viewer.svelte

@@ -4,19 +4,16 @@
   import { downloadArchive } from '$lib/utils/asset-utils';
   import { api, AssetResponseDto, SharedLinkResponseDto } from '@api';
   import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
-  import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
-  import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte';
-  import FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import DownloadAction from '../photos-page/actions/download-action.svelte';
   import RemoveFromSharedLink from '../photos-page/actions/remove-from-shared-link.svelte';
   import AssetSelectControlBar from '../photos-page/asset-select-control-bar.svelte';
   import ControlAppBar from '../shared-components/control-app-bar.svelte';
   import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
-  import SelectAll from 'svelte-material-icons/SelectAll.svelte';
   import ImmichLogo from '../shared-components/immich-logo.svelte';
   import { notificationController, NotificationType } from '../shared-components/notification/notification';
   import { handleError } from '$lib/utils/handle-error';
+  import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js';
 
   export let sharedLink: SharedLinkResponseDto;
   export let isOwned: boolean;
@@ -72,7 +69,7 @@
 <section class="bg-immich-bg dark:bg-immich-dark-bg">
   {#if isMultiSelectionMode}
     <AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
-      <CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
+      <CircleIconButton title="Select all" icon={mdiSelectAll} on:click={handleSelectAll} />
       {#if sharedLink?.allowDownload}
         <DownloadAction filename="immich-shared.zip" />
       {/if}
@@ -81,7 +78,7 @@
       {/if}
     </AssetSelectControlBar>
   {:else}
-    <ControlAppBar on:close-button-click={() => goto('/photos')} backIcon={ArrowLeft} showBackButton={false}>
+    <ControlAppBar on:close-button-click={() => goto('/photos')} backIcon={mdiArrowLeft} showBackButton={false}>
       <svelte:fragment slot="leading">
         <a
           data-sveltekit-preload-data="hover"
@@ -95,11 +92,11 @@
 
       <svelte:fragment slot="trailing">
         {#if sharedLink?.allowUpload}
-          <CircleIconButton title="Add Photos" on:click={() => handleUploadAssets()} logo={FileImagePlusOutline} />
+          <CircleIconButton title="Add Photos" on:click={() => handleUploadAssets()} icon={mdiFileImagePlusOutline} />
         {/if}
 
         {#if sharedLink?.allowDownload}
-          <CircleIconButton title="Download" on:click={downloadAssets} logo={FolderDownloadOutline} />
+          <CircleIconButton title="Download" on:click={downloadAssets} icon={mdiFolderDownloadOutline} />
         {/if}
       </svelte:fragment>
     </ControlAppBar>

+ 3 - 2
web/src/lib/components/shared-components/album-selection-modal.svelte

@@ -1,9 +1,10 @@
 <script lang="ts">
   import { AlbumResponseDto, api } from '@api';
   import { createEventDispatcher, onMount } from 'svelte';
-  import Plus from 'svelte-material-icons/Plus.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import BaseModal from './base-modal.svelte';
   import AlbumListItem from '../asset-viewer/album-list-item.svelte';
+  import { mdiPlus } from '@mdi/js';
 
   let albums: AlbumResponseDto[] = [];
   let recentAlbums: AlbumResponseDto[] = [];
@@ -84,7 +85,7 @@
           class="flex w-full items-center gap-4 px-6 py-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700"
         >
           <div class="flex h-12 w-12 items-center justify-center">
-            <Plus size="30" />
+            <Icon path={mdiPlus} size="30" />
           </div>
           <p class="">
             New {#if shared}Shared {/if}Album {#if search.length > 0}<b>{search}</b>{/if}

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

@@ -1,11 +1,11 @@
 <script lang="ts">
   import { fade } from 'svelte/transition';
   import { quintOut } from 'svelte/easing';
-  import Close from 'svelte-material-icons/Close.svelte';
   import { createEventDispatcher, onMount, onDestroy } from 'svelte';
   import { browser } from '$app/environment';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import { clickOutside } from '$lib/utils/click-outside';
+  import { mdiClose } from '@mdi/js';
 
   const dispatch = createEventDispatcher();
   export let zIndex = 9999;
@@ -46,7 +46,7 @@
         </slot>
       </div>
 
-      <CircleIconButton on:click={() => dispatch('close')} logo={Close} size={'20'} />
+      <CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} size={'20'} />
     </div>
 
     <div class="">

+ 3 - 3
web/src/lib/components/shared-components/control-app-bar.svelte

@@ -2,12 +2,12 @@
   import { browser } from '$app/environment';
 
   import { createEventDispatcher, onDestroy, onMount } from 'svelte';
-  import Close from 'svelte-material-icons/Close.svelte';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import { fly } from 'svelte/transition';
+  import { mdiClose } from '@mdi/js';
 
   export let showBackButton = true;
-  export let backIcon = Close;
+  export let backIcon = mdiClose;
   export let tailwindClasses = '';
   export let forceDark = false;
 
@@ -51,7 +51,7 @@
       {#if showBackButton}
         <CircleIconButton
           on:click={() => dispatch('close-button-click')}
-          logo={backIcon}
+          icon={backIcon}
           backgroundColor={'transparent'}
           hoverColor={'#e2e7e9'}
           size={'24'}

+ 3 - 2
web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte

@@ -7,11 +7,12 @@
   import { handleError } from '$lib/utils/handle-error';
   import { api, copyToClipboard, SharedLinkResponseDto, SharedLinkType } from '@api';
   import { createEventDispatcher, onMount } from 'svelte';
-  import Link from 'svelte-material-icons/Link.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import BaseModal from '../base-modal.svelte';
   import type { ImmichDropDownOption } from '../dropdown-button.svelte';
   import DropdownButton from '../dropdown-button.svelte';
   import { notificationController, NotificationType } from '../notification/notification';
+  import { mdiLink } from '@mdi/js';
 
   export let albumId: string | undefined = undefined;
   export let assetIds: string[] = [];
@@ -140,7 +141,7 @@
 <BaseModal on:close={() => dispatch('close')} on:escape={() => dispatch('escape')}>
   <svelte:fragment slot="title">
     <span class="flex place-items-center gap-2">
-      <Link size={24} />
+      <Icon path={mdiLink} size={24} />
       {#if editingLink}
         <p class="font-medium text-immich-fg dark:text-immich-dark-fg">Edit link</p>
       {:else}

+ 4 - 4
web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte

@@ -3,10 +3,10 @@
   import { AppRoute } from '$lib/constants';
   import type { UserResponseDto } from '@api';
   import { createEventDispatcher } from 'svelte';
-  import Cog from 'svelte-material-icons/Cog.svelte';
-  import Logout from 'svelte-material-icons/Logout.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import { fade } from 'svelte/transition';
   import UserAvatar from '../user-avatar.svelte';
+  import { mdiCog, mdiLogout } from '@mdi/js';
 
   export let user: UserResponseDto;
 
@@ -35,7 +35,7 @@
     <a href={AppRoute.USER_SETTINGS} on:click={() => dispatch('close')}>
       <Button color="dark-gray" size="sm" shadow={false} border>
         <div class="flex place-content-center place-items-center gap-2 px-2">
-          <Cog size="18" />
+          <Icon path={mdiCog} size="18" />
           Account Settings
         </div>
       </Button>
@@ -47,7 +47,7 @@
       class="flex w-full place-content-center place-items-center gap-2 py-3 font-medium text-gray-500 hover:bg-immich-primary/10 dark:text-gray-300"
       on:click={() => dispatch('logout')}
     >
-      <Logout size={24} />
+      <Icon path={mdiLogout} size={24} />
       Sign Out</button
     >
   </div>

+ 6 - 6
web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte

@@ -4,7 +4,6 @@
   import { clickOutside } from '$lib/utils/click-outside';
   import { createEventDispatcher } from 'svelte';
   import { fade, fly } from 'svelte/transition';
-  import TrayArrowUp from 'svelte-material-icons/TrayArrowUp.svelte';
   import { api, UserResponseDto } from '@api';
   import ThemeButton from '../theme-button.svelte';
   import { AppRoute } from '../../../constants';
@@ -12,11 +11,11 @@
   import ImmichLogo from '../immich-logo.svelte';
   import SearchBar from '../search-bar/search-bar.svelte';
   import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
-  import Magnify from 'svelte-material-icons/Magnify.svelte';
   import IconButton from '$lib/components/elements/buttons/icon-button.svelte';
-  import Cog from 'svelte-material-icons/Cog.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import UserAvatar from '../user-avatar.svelte';
   import { featureFlags } from '$lib/stores/server-config.store';
+  import { mdiMagnify, mdiTrayArrowUp, mdiCog } from '@mdi/js';
   export let user: UserResponseDto;
   export let showUploadButton = true;
 
@@ -56,7 +55,7 @@
           <a href={AppRoute.SEARCH} id="search-button" class="pl-4 sm:hidden">
             <IconButton title="Search">
               <div class="flex gap-2">
-                <Magnify size="1.5em" />
+                <Icon path={mdiMagnify} size="1.5em" />
               </div>
             </IconButton>
           </a>
@@ -68,7 +67,7 @@
           <div in:fly={{ x: 50, duration: 250 }}>
             <LinkButton on:click={() => dispatch('uploadClicked')}>
               <div class="flex gap-2">
-                <TrayArrowUp size="1.5em" />
+                <Icon path={mdiTrayArrowUp} size="1.5em" />
                 <span class="hidden md:block">Upload</span>
               </div>
             </LinkButton>
@@ -95,7 +94,8 @@
                 </span>
               </div>
               <div class="block sm:hidden" aria-hidden="true">
-                <Cog
+                <Icon
+                  path={mdiCog}
                   size="1.5em"
                   class="dark:text-immich-dark-fg {$page.url.pathname.includes('/admin')
                     ? 'text-immich-primary dark:text-immich-dark-primary'

+ 5 - 7
web/src/lib/components/shared-components/notification/notification-card.svelte

@@ -1,15 +1,13 @@
 <script lang="ts">
   import { fade } from 'svelte/transition';
-  import CloseCircleOutline from 'svelte-material-icons/CloseCircleOutline.svelte';
-  import InformationOutline from 'svelte-material-icons/InformationOutline.svelte';
-  import WindowClose from 'svelte-material-icons/WindowClose.svelte';
-
+  import Icon from '$lib/components/elements/icon.svelte';
   import {
     ImmichNotification,
     notificationController,
     NotificationType,
   } from '$lib/components/shared-components/notification/notification';
   import { onMount } from 'svelte';
+  import { mdiCloseCircleOutline, mdiInformationOutline, mdiWindowClose } from '@mdi/js';
 
   export let notificationInfo: ImmichNotification;
 
@@ -17,7 +15,7 @@
   let errorPrimaryColor = '#E64132';
   let warningPrimaryColor = '#D08613';
 
-  $: icon = notificationInfo.type === NotificationType.Error ? CloseCircleOutline : InformationOutline;
+  $: icon = notificationInfo.type === NotificationType.Error ? mdiCloseCircleOutline : mdiInformationOutline;
 
   $: backgroundColor = () => {
     if (notificationInfo.type === NotificationType.Info) {
@@ -93,13 +91,13 @@
 >
   <div class="flex justify-between">
     <div class="flex place-items-center gap-2">
-      <svelte:component this={icon} color={primaryColor()} size="20" />
+      <Icon path={icon} color={primaryColor()} size="20" />
       <h2 style:color={primaryColor()} class="font-medium" data-testid="title">
         {notificationInfo.type.toString()}
       </h2>
     </div>
     <button on:click|stopPropagation={discard}>
-      <svelte:component this={WindowClose} size="20" />
+      <Icon path={mdiWindowClose} size="20" />
     </button>
   </div>
 

+ 8 - 6
web/src/lib/components/shared-components/search-bar/search-bar.svelte

@@ -1,11 +1,11 @@
 <script lang="ts">
   import { AppRoute } from '$lib/constants';
-  import Magnify from 'svelte-material-icons/Magnify.svelte';
-  import Close from 'svelte-material-icons/Close.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import { goto } from '$app/navigation';
   import { isSearchEnabled, preventRaceConditionSearchBar, savedSearchTerms } from '$lib/stores/search.store';
   import { fly } from 'svelte/transition';
   import { clickOutside } from '$lib/utils/click-outside';
+  import { mdiClose, mdiMagnify } from '@mdi/js';
   export let value = '';
   export let grayTheme: boolean;
 
@@ -82,7 +82,7 @@
       <div class="absolute inset-y-0 left-0 flex items-center pl-6">
         <div class="dark:text-immich-dark-fg/75">
           <button class="flex items-center">
-            <Magnify size="1.5em" />
+            <Icon path={mdiMagnify} size="1.5em" />
           </button>
         </div>
       </div>
@@ -108,7 +108,7 @@
           type="reset"
           class="rounded-full p-2 hover:bg-immich-primary/5 active:bg-immich-primary/10 dark:text-immich-dark-fg/75 dark:hover:bg-immich-dark-primary/25 dark:active:bg-immich-dark-primary/[.35]"
         >
-          <Close size="1.5em" />
+          <Icon path={mdiClose} size="1.5em" />
         </button>
       </div>
     {/if}
@@ -153,11 +153,13 @@
                   onSearch();
                 }}
               >
-                <Magnify size="1.5em" />
+                <Icon path={mdiMagnify} size="1.5em" />
                 {savedSearchTerm}
               </button>
               <div class="absolute right-5 top-0 items-center justify-center py-3">
-                <button type="button" on:click={() => clearSearchTerm(savedSearchTerm)}><Close size="18" /></button>
+                <button type="button" on:click={() => clearSearchTerm(savedSearchTerm)}
+                  ><Icon path={mdiClose} size="18" /></button
+                >
               </div>
             </div>
           </div>

+ 2 - 2
web/src/lib/components/shared-components/show-shortcuts.svelte

@@ -1,8 +1,8 @@
 <script lang="ts">
-  import Close from 'svelte-material-icons/Close.svelte';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import { createEventDispatcher } from 'svelte';
   import FullScreenModal from './full-screen-modal.svelte';
+  import { mdiClose } from '@mdi/js';
 
   const shortcuts = {
     general: [
@@ -30,7 +30,7 @@
       <div class="relative px-4 pt-4">
         <h1 class="px-4 py-4 font-medium text-immich-primary dark:text-immich-dark-primary">Keyboard Shortcuts</h1>
         <div class="absolute inset-y-0 right-0 px-4 py-4">
-          <CircleIconButton logo={Close} on:click={() => dispatch('close')} />
+          <CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
         </div>
       </div>
 

+ 6 - 10
web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte

@@ -4,32 +4,28 @@
   import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
   import StatusBox from '$lib/components/shared-components/status-box.svelte';
   import { AppRoute } from '$lib/constants';
-  import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
-  import Cog from 'svelte-material-icons/Cog.svelte';
-  import Server from 'svelte-material-icons/Server.svelte';
-  import Tools from 'svelte-material-icons/Tools.svelte';
-  import Sync from 'svelte-material-icons/Sync.svelte';
+  import { mdiAccountMultipleOutline, mdiCog, mdiServer, mdiSync, mdiTools } from '@mdi/js';
 </script>
 
 <SideBarSection>
   <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_USER_MANAGEMENT} draggable="false">
     <SideBarButton
       title="Users"
-      logo={AccountMultipleOutline}
+      icon={mdiAccountMultipleOutline}
       isSelected={$page.route.id === AppRoute.ADMIN_USER_MANAGEMENT}
     />
   </a>
   <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_JOBS} draggable="false">
-    <SideBarButton title="Jobs" logo={Sync} isSelected={$page.route.id === AppRoute.ADMIN_JOBS} />
+    <SideBarButton title="Jobs" icon={mdiSync} isSelected={$page.route.id === AppRoute.ADMIN_JOBS} />
   </a>
   <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_SETTINGS} draggable="false">
-    <SideBarButton title="Settings" logo={Cog} isSelected={$page.route.id === AppRoute.ADMIN_SETTINGS} />
+    <SideBarButton title="Settings" icon={mdiCog} isSelected={$page.route.id === AppRoute.ADMIN_SETTINGS} />
   </a>
   <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_STATS} draggable="false">
-    <SideBarButton title="Server Stats" logo={Server} isSelected={$page.route.id === AppRoute.ADMIN_STATS} />
+    <SideBarButton title="Server Stats" icon={mdiServer} isSelected={$page.route.id === AppRoute.ADMIN_STATS} />
   </a>
   <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_REPAIR} draggable="false">
-    <SideBarButton title="Repair" logo={Tools} isSelected={$page.route.id === AppRoute.ADMIN_REPAIR} />
+    <SideBarButton title="Repair" icon={mdiTools} isSelected={$page.route.id === AppRoute.ADMIN_REPAIR} />
   </a>
   <div class="mb-6 mt-auto">
     <StatusBox />

+ 6 - 6
web/src/lib/components/shared-components/side-bar/side-bar-button.svelte

@@ -1,11 +1,11 @@
 <script lang="ts">
-  import type Icon from 'svelte-material-icons/AbTesting.svelte';
-  import InformationOutline from 'svelte-material-icons/InformationOutline.svelte';
-  import { fade } from 'svelte/transition';
   import { createEventDispatcher } from 'svelte';
+  import { fade } from 'svelte/transition';
+  import Icon from '$lib/components/elements/icon.svelte';
+  import { mdiInformationOutline } from '@mdi/js';
 
   export let title: string;
-  export let logo: typeof Icon;
+  export let icon: string;
   export let isSelected: boolean;
   export let flippedLogo = false;
 
@@ -27,7 +27,7 @@
   "
 >
   <div class="flex w-full place-items-center gap-4 overflow-hidden truncate">
-    <svelte:component this={logo} size="1.5em" class="shrink-0 {flippedLogo ? '-scale-x-100' : ''}" />
+    <Icon path={icon} size="1.5em" class="shrink-0" flipped={flippedLogo} />
     <p class="text-sm font-medium">{title}</p>
   </div>
 
@@ -41,7 +41,7 @@
         on:mouseleave={() => (showMoreInformation = false)}
       >
         <div class="p-1 text-gray-600 hover:cursor-help dark:text-gray-400">
-          <InformationOutline />
+          <Icon path={mdiInformationOutline} />
         </div>
 
         {#if showMoreInformation}

+ 30 - 23
web/src/lib/components/shared-components/side-bar/side-bar.svelte

@@ -1,25 +1,27 @@
 <script lang="ts">
   import { page } from '$app/stores';
+  import { locale, sidebarSettings } from '$lib/stores/preferences.store';
+  import { featureFlags } from '$lib/stores/server-config.store';
   import { AssetApiGetAssetStatsRequest, api } from '@api';
-  import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
-  import AccountMultiple from 'svelte-material-icons/AccountMultiple.svelte';
-  import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
-  import ImageMultipleOutline from 'svelte-material-icons/ImageMultipleOutline.svelte';
-  import ImageMultiple from 'svelte-material-icons/ImageMultiple.svelte';
-  import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
-  import Magnify from 'svelte-material-icons/Magnify.svelte';
-  import Map from 'svelte-material-icons/Map.svelte';
-  import Account from 'svelte-material-icons/Account.svelte';
-  import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
-  import HeartMultipleOutline from 'svelte-material-icons/HeartMultipleOutline.svelte';
-  import HeartMultiple from 'svelte-material-icons/HeartMultiple.svelte';
+  import {
+    mdiAccount,
+    mdiAccountMultiple,
+    mdiAccountMultipleOutline,
+    mdiArchiveArrowDownOutline,
+    mdiHeartMultiple,
+    mdiHeartMultipleOutline,
+    mdiImageAlbum,
+    mdiImageMultiple,
+    mdiImageMultipleOutline,
+    mdiMagnify,
+    mdiMap,
+    mdiTrashCanOutline,
+  } from '@mdi/js';
   import { AppRoute } from '../../../constants';
   import LoadingSpinner from '../loading-spinner.svelte';
   import StatusBox from '../status-box.svelte';
   import SideBarButton from './side-bar-button.svelte';
-  import { locale, sidebarSettings } from '$lib/stores/preferences.store';
   import SideBarSection from './side-bar-section.svelte';
-  import { featureFlags } from '$lib/stores/server-config.store';
 
   const getStats = async (dto: AssetApiGetAssetStatsRequest) => {
     const { data: stats } = await api.assetApi.getAssetStats(dto);
@@ -45,7 +47,7 @@
   <a data-sveltekit-preload-data="hover" data-sveltekit-noscroll href={AppRoute.PHOTOS} draggable="false">
     <SideBarButton
       title="Photos"
-      logo={isPhotosSelected ? ImageMultiple : ImageMultipleOutline}
+      icon={isPhotosSelected ? mdiImageMultiple : mdiImageMultipleOutline}
       isSelected={isPhotosSelected}
     >
       <svelte:fragment slot="moreInformation">
@@ -62,23 +64,23 @@
   </a>
   {#if $featureFlags.search}
     <a data-sveltekit-preload-data="hover" data-sveltekit-noscroll href={AppRoute.EXPLORE} draggable="false">
-      <SideBarButton title="Explore" logo={Magnify} isSelected={$page.route.id === '/(user)/explore'} />
+      <SideBarButton title="Explore" icon={mdiMagnify} isSelected={$page.route.id === '/(user)/explore'} />
     </a>
   {/if}
   {#if $featureFlags.map}
     <a data-sveltekit-preload-data="hover" href={AppRoute.MAP} draggable="false">
-      <SideBarButton title="Map" logo={Map} isSelected={$page.route.id === '/(user)/map'} />
+      <SideBarButton title="Map" icon={mdiMap} isSelected={$page.route.id === '/(user)/map'} />
     </a>
   {/if}
   {#if $sidebarSettings.people}
     <a data-sveltekit-preload-data="hover" href={AppRoute.PEOPLE} draggable="false">
-      <SideBarButton title="People" logo={Account} isSelected={$page.route.id === '/(user)/people'} />
+      <SideBarButton title="People" icon={mdiAccount} isSelected={$page.route.id === '/(user)/people'} />
     </a>
   {/if}
   <a data-sveltekit-preload-data="hover" href={AppRoute.SHARING} draggable="false">
     <SideBarButton
       title="Sharing"
-      logo={isSharingSelected ? AccountMultiple : AccountMultipleOutline}
+      icon={isSharingSelected ? mdiAccountMultiple : mdiAccountMultipleOutline}
       isSelected={isSharingSelected}
     >
       <svelte:fragment slot="moreInformation">
@@ -100,7 +102,7 @@
   <a data-sveltekit-preload-data="hover" href={AppRoute.FAVORITES} draggable="false">
     <SideBarButton
       title="Favorites"
-      logo={isFavoritesSelected ? HeartMultiple : HeartMultipleOutline}
+      icon={isFavoritesSelected ? mdiHeartMultiple : mdiHeartMultipleOutline}
       isSelected={isFavoritesSelected}
     >
       <svelte:fragment slot="moreInformation">
@@ -116,7 +118,12 @@
     </SideBarButton>
   </a>
   <a data-sveltekit-preload-data="hover" href={AppRoute.ALBUMS} draggable="false">
-    <SideBarButton title="Albums" logo={ImageAlbum} flippedLogo={true} isSelected={$page.route.id === '/(user)/albums'}>
+    <SideBarButton
+      title="Albums"
+      icon={mdiImageAlbum}
+      flippedLogo={true}
+      isSelected={$page.route.id === '/(user)/albums'}
+    >
       <svelte:fragment slot="moreInformation">
         {#await getAlbumCount()}
           <LoadingSpinner />
@@ -129,7 +136,7 @@
     </SideBarButton>
   </a>
   <a data-sveltekit-preload-data="hover" href={AppRoute.ARCHIVE} draggable="false">
-    <SideBarButton title="Archive" logo={ArchiveArrowDownOutline} isSelected={$page.route.id === '/(user)/archive'}>
+    <SideBarButton title="Archive" icon={mdiArchiveArrowDownOutline} isSelected={$page.route.id === '/(user)/archive'}>
       <svelte:fragment slot="moreInformation">
         {#await getStats({ isArchived: true })}
           <LoadingSpinner />
@@ -145,7 +152,7 @@
 
   {#if $featureFlags.trash}
     <a data-sveltekit-preload-data="hover" href={AppRoute.TRASH} draggable="false">
-      <SideBarButton title="Trash" logo={TrashCanOutline} isSelected={isTrashSelected}>
+      <SideBarButton title="Trash" icon={mdiTrashCanOutline} isSelected={isTrashSelected}>
         <svelte:fragment slot="moreInformation">
           {#await getStats({ isTrashed: true })}
             <LoadingSpinner />

+ 4 - 4
web/src/lib/components/shared-components/status-box.svelte

@@ -4,10 +4,10 @@
   import { websocketStore } from '$lib/stores/websocket';
   import { ServerInfoResponseDto, api } from '@api';
   import { onDestroy, onMount } from 'svelte';
-  import Cloud from 'svelte-material-icons/Cloud.svelte';
-  import Dns from 'svelte-material-icons/Dns.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import { asByteUnitString } from '../../utils/byte-units';
   import LoadingSpinner from './loading-spinner.svelte';
+  import { mdiCloud, mdiDns } from '@mdi/js';
 
   const { serverVersion, connected } = websocketStore;
 
@@ -40,7 +40,7 @@
 <div class="dark:text-immich-dark-fg">
   <div class="storage-status grid grid-cols-[64px_auto]">
     <div class="pb-[2.15rem] pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary group-hover:sm:pb-0 md:pb-0">
-      <Cloud size={'24'} />
+      <Icon path={mdiCloud} size={'24'} />
     </div>
     <div class="hidden group-hover:sm:block md:block">
       <p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">Storage</p>
@@ -68,7 +68,7 @@
   </div>
   <div class="server-status grid grid-cols-[64px_auto]">
     <div class="pb-11 pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary group-hover:sm:pb-0 md:pb-0">
-      <Dns size={'24'} />
+      <Icon path={mdiDns} size={'24'} />
     </div>
     <div class="hidden text-xs group-hover:sm:block md:block">
       <p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">Server</p>

+ 4 - 4
web/src/lib/components/shared-components/upload-asset-preview.svelte

@@ -7,9 +7,9 @@
   import ImmichLogo from './immich-logo.svelte';
   import { getFilenameExtension } from '$lib/utils/asset-utils';
   import { uploadAssetsStore } from '$lib/stores/upload';
-  import Cancel from 'svelte-material-icons/Cancel.svelte';
-  import Refresh from 'svelte-material-icons/Refresh.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import { fileUploadHandler } from '$lib/utils/file-uploader';
+  import { mdiRefresh, mdiCancel } from '@mdi/js';
 
   export let uploadAsset: UploadAsset;
 
@@ -84,14 +84,14 @@
           title="Retry upload"
           class="flex h-full w-full place-content-center place-items-center text-sm"
         >
-          <span class="text-immich-dark-gray dark:text-immich-dark-fg"><Refresh size="20" /></span>
+          <span class="text-immich-dark-gray dark:text-immich-dark-fg"><Icon path={mdiRefresh} size="20" /></span>
         </button>
         <button
           on:click={() => uploadAssetsStore.removeUploadAsset(uploadAsset.id)}
           title="Dismiss error"
           class="flex h-full w-full place-content-center place-items-center text-sm"
         >
-          <span class="text-immich-error"><Cancel size="20" /></span>
+          <span class="text-immich-error"><Icon path={mdiCancel} size="20" /></span>
         </button>
       </div>
     {/if}

+ 6 - 8
web/src/lib/components/shared-components/upload-panel.svelte

@@ -2,14 +2,12 @@
   import { quartInOut } from 'svelte/easing';
   import { fade, scale } from 'svelte/transition';
   import { uploadAssetsStore } from '$lib/stores/upload';
-  import CloudUploadOutline from 'svelte-material-icons/CloudUploadOutline.svelte';
-  import WindowMinimize from 'svelte-material-icons/WindowMinimize.svelte';
-  import Cancel from 'svelte-material-icons/Cancel.svelte';
-  import Cog from 'svelte-material-icons/Cog.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import { notificationController, NotificationType } from './notification/notification';
   import UploadAssetPreview from './upload-asset-preview.svelte';
   import { uploadExecutionQueue } from '$lib/utils/file-uploader';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
+  import { mdiCog, mdiWindowMinimize, mdiCancel, mdiCloudUploadOutline } from '@mdi/js';
 
   let showDetail = false;
   let showOptions = false;
@@ -75,14 +73,14 @@
             <div class="flex flex-row">
               <CircleIconButton
                 title="Toggle settings"
-                logo={Cog}
+                icon={mdiCog}
                 size="14"
                 padding="1"
                 on:click={() => (showOptions = !showOptions)}
               />
               <CircleIconButton
                 title="Minimize"
-                logo={WindowMinimize}
+                icon={mdiWindowMinimize}
                 size="14"
                 padding="1"
                 on:click={() => (showDetail = false)}
@@ -91,7 +89,7 @@
             {#if $hasError}
               <CircleIconButton
                 title="Dismiss all errors"
-                logo={Cancel}
+                icon={mdiCancel}
                 size="14"
                 padding="1"
                 on:click={() => uploadAssetsStore.dismissErrors()}
@@ -148,7 +146,7 @@
           class="flex h-16 w-16 place-content-center place-items-center rounded-full bg-gray-200 p-5 text-sm text-immich-primary shadow-lg dark:bg-gray-600 dark:text-immich-gray"
         >
           <div class="animate-pulse">
-            <CloudUploadOutline size="30" />
+            <Icon path={mdiCloudUploadOutline} size="30" />
           </div>
         </button>
       </div>

+ 6 - 8
web/src/lib/components/sharedlinks-page/shared-link-card.svelte

@@ -1,14 +1,12 @@
 <script lang="ts">
   import { api, AssetResponseDto, SharedLinkResponseDto, SharedLinkType, ThumbnailFormat } from '@api';
   import LoadingSpinner from '../shared-components/loading-spinner.svelte';
-  import OpenInNew from 'svelte-material-icons/OpenInNew.svelte';
-  import Delete from 'svelte-material-icons/TrashCanOutline.svelte';
-  import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
-  import CircleEditOutline from 'svelte-material-icons/CircleEditOutline.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import * as luxon from 'luxon';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import { createEventDispatcher } from 'svelte';
   import { goto } from '$app/navigation';
+  import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js';
 
   export let link: SharedLinkResponseDto;
 
@@ -110,7 +108,7 @@
               on:click={() => goto(`/share/${link.key}`)}
               on:keydown={() => goto(`/share/${link.key}`)}
             >
-              <OpenInNew />
+              <Icon path={mdiOpenInNew} />
             </div>
           {/if}
         </div>
@@ -148,9 +146,9 @@
 
   <div class="flex flex-auto flex-col place-content-center place-items-end text-right">
     <div class="flex">
-      <CircleIconButton logo={Delete} on:click={() => dispatch('delete')} />
-      <CircleIconButton logo={CircleEditOutline} on:click={() => dispatch('edit')} />
-      <CircleIconButton logo={ContentCopy} on:click={() => dispatch('copy')} />
+      <CircleIconButton icon={mdiDelete} on:click={() => dispatch('delete')} />
+      <CircleIconButton icon={mdiCircleEditOutline} on:click={() => dispatch('edit')} />
+      <CircleIconButton icon={mdiContentCopy} on:click={() => dispatch('copy')} />
     </div>
   </div>
 </div>

+ 19 - 16
web/src/lib/components/user-settings-page/device-card.svelte

@@ -3,14 +3,17 @@
   import type { AuthDeviceResponseDto } from '@api';
   import { DateTime, ToRelativeCalendarOptions } from 'luxon';
   import { createEventDispatcher } from 'svelte';
-  import Android from 'svelte-material-icons/Android.svelte';
-  import Apple from 'svelte-material-icons/Apple.svelte';
-  import AppleSafari from 'svelte-material-icons/AppleSafari.svelte';
-  import GoogleChrome from 'svelte-material-icons/GoogleChrome.svelte';
-  import Help from 'svelte-material-icons/Help.svelte';
-  import Linux from 'svelte-material-icons/Linux.svelte';
-  import MicrosoftWindows from 'svelte-material-icons/MicrosoftWindows.svelte';
-  import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
+  import {
+    mdiAndroid,
+    mdiApple,
+    mdiAppleSafari,
+    mdiMicrosoftWindows,
+    mdiLinux,
+    mdiGoogleChrome,
+    mdiTrashCanOutline,
+    mdiHelp,
+  } from '@mdi/js';
 
   export let device: AuthDeviceResponseDto;
 
@@ -26,19 +29,19 @@
   <!-- TODO: Device Image -->
   <div class="hidden items-center justify-center pr-2 text-immich-primary dark:text-immich-dark-primary sm:flex">
     {#if device.deviceOS === 'Android'}
-      <Android size="40" />
+      <Icon path={mdiAndroid} size="40" />
     {:else if device.deviceOS === 'iOS' || device.deviceOS === 'Mac OS'}
-      <Apple size="40" />
+      <Icon path={mdiApple} size="40" />
     {:else if device.deviceOS.indexOf('Safari') !== -1}
-      <AppleSafari size="40" />
+      <Icon path={mdiAppleSafari} size="40" />
     {:else if device.deviceOS.indexOf('Windows') !== -1}
-      <MicrosoftWindows size="40" />
+      <Icon path={mdiMicrosoftWindows} size="40" />
     {:else if device.deviceOS === 'Linux'}
-      <Linux size="40" />
+      <Icon path={mdiLinux} size="40" />
     {:else if device.deviceOS === 'Chromium OS' || device.deviceType === 'Chrome' || device.deviceType === 'Chromium'}
-      <GoogleChrome size="40" />
+      <Icon path={mdiGoogleChrome} size="40" />
     {:else}
-      <Help size="40" />
+      <Icon path={mdiHelp} size="40" />
     {/if}
   </div>
   <div class="flex grow flex-row justify-between gap-1 pl-4 sm:pl-0">
@@ -62,7 +65,7 @@
           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"
           title="Log out"
         >
-          <TrashCanOutline size="16" />
+          <Icon path={mdiTrashCanOutline} size="16" />
         </button>
       </div>
     {/if}

+ 5 - 6
web/src/lib/components/user-settings-page/library-list.svelte

@@ -6,9 +6,7 @@
   import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
   import { handleError } from '$lib/utils/handle-error';
   import { fade } from 'svelte/transition';
-  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
-  import Database from 'svelte-material-icons/Database.svelte';
-  import Upload from 'svelte-material-icons/Upload.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import Pulse from 'svelte-loading-spinners/Pulse.svelte';
   import { slide } from 'svelte/transition';
   import LibraryImportPathsForm from '../forms/library-import-paths-form.svelte';
@@ -19,6 +17,7 @@
   import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
   import MenuOption from '../shared-components/context-menu/menu-option.svelte';
   import { getContextMenuPosition } from '$lib/utils/context-menu';
+  import { mdiDatabase, mdiDotsVertical, mdiUpload } from '@mdi/js';
 
   let libraries: LibraryResponseDto[] = [];
 
@@ -317,9 +316,9 @@
             >
               <td class="w-1/6 px-10 text-sm">
                 {#if library.type === LibraryType.External}
-                  <Database size="40" title="External library (created on {library.createdAt})" />
+                  <Icon path={mdiDatabase} size="40" title="External library (created on {library.createdAt})" />
                 {:else if library.type === LibraryType.Upload}
-                  <Upload size="40" title="Upload library (created on {library.createdAt})" />
+                  <Icon path={mdiUpload} size="40" title="Upload library (created on {library.createdAt})" />
                 {/if}</td
               >
 
@@ -340,7 +339,7 @@
                   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"
                   on:click|stopPropagation|preventDefault={(e) => showMenu(e, library, index)}
                 >
-                  <DotsVertical size="16" />
+                  <Icon path={mdiDotsVertical} size="16" />
                 </button>
 
                 {#if showContextMenu}

+ 2 - 2
web/src/lib/components/user-settings-page/partner-settings.svelte

@@ -1,12 +1,12 @@
 <script lang="ts">
   import { UserResponseDto, api } from '@api';
   import UserAvatar from '../shared-components/user-avatar.svelte';
-  import Close from 'svelte-material-icons/Close.svelte';
   import Button from '../elements/buttons/button.svelte';
   import PartnerSelectionModal from './partner-selection-modal.svelte';
   import { handleError } from '../../utils/handle-error';
   import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
+  import { mdiClose } from '@mdi/js';
 
   export let user: UserResponseDto;
 
@@ -64,7 +64,7 @@
           </div>
           <CircleIconButton
             on:click={() => (removePartner = partner)}
-            logo={Close}
+            icon={mdiClose}
             size={'16'}
             title="Remove partner"
           />

+ 4 - 4
web/src/lib/components/user-settings-page/user-api-key-list.svelte

@@ -1,7 +1,6 @@
 <script lang="ts">
   import { api, APIKeyResponseDto } from '@api';
-  import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
-  import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import { fade } from 'svelte/transition';
   import { handleError } from '../../utils/handle-error';
   import APIKeyForm from '../forms/api-key-form.svelte';
@@ -10,6 +9,7 @@
   import { notificationController, NotificationType } from '../shared-components/notification/notification';
   import { locale } from '$lib/stores/preferences.store';
   import Button from '../elements/buttons/button.svelte';
+  import { mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
 
   export let keys: APIKeyResponseDto[];
 
@@ -143,13 +143,13 @@
                     on:click={() => (editKey = key)}
                     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"
                   >
-                    <PencilOutline size="16" />
+                    <Icon path={mdiPencilOutline} size="16" />
                   </button>
                   <button
                     on:click={() => (deleteKey = key)}
                     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"
                   >
-                    <TrashCanOutline size="16" />
+                    <Icon path={mdiTrashCanOutline} size="16" />
                   </button>
                 </td>
               </tr>

+ 18 - 15
web/src/routes/(user)/albums/+page.svelte

@@ -16,11 +16,7 @@
   import { goto } from '$app/navigation';
   import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
   import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
-  import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
-  import FormatListBulletedSquare from 'svelte-material-icons/FormatListBulletedSquare.svelte';
-  import ViewGridOutline from 'svelte-material-icons/ViewGridOutline.svelte';
   import type { PageData } from './$types';
-  import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
   import { useAlbums } from './albums.bloc';
   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
@@ -37,13 +33,20 @@
   } from '$lib/components/shared-components/notification/notification';
   import type { AlbumResponseDto } from '@api';
   import TableHeader from '$lib/components/elements/table-header.svelte';
-  import ArrowDownThin from 'svelte-material-icons/ArrowDownThin.svelte';
-  import ArrowUpThin from 'svelte-material-icons/ArrowUpThin.svelte';
-  import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import EditAlbumForm from '$lib/components/forms/edit-album-form.svelte';
-  import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import { orderBy } from 'lodash-es';
+  import {
+    mdiPlusBoxOutline,
+    mdiArrowDownThin,
+    mdiArrowUpThin,
+    mdiFormatListBulletedSquare,
+    mdiPencilOutline,
+    mdiTrashCanOutline,
+    mdiViewGridOutline,
+    mdiDeleteOutline,
+  } from '@mdi/js';
 
   export let data: PageData;
   let shouldShowEditUserForm = false;
@@ -210,7 +213,7 @@
   <div class="flex place-items-center gap-2" slot="buttons">
     <LinkButton on:click={handleCreateAlbum}>
       <div class="flex place-items-center gap-2 text-sm">
-        <PlusBoxOutline size="18" />
+        <Icon path={mdiPlusBoxOutline} size="18" />
         Create album
       </div>
     </LinkButton>
@@ -220,7 +223,7 @@
       render={(option) => {
         return {
           title: option.sortTitle,
-          icon: option.sortDesc ? ArrowDownThin : ArrowUpThin,
+          icon: option.sortDesc ? mdiArrowDownThin : mdiArrowUpThin,
         };
       }}
       on:select={(event) => {
@@ -236,10 +239,10 @@
     <LinkButton on:click={() => handleChangeListMode()}>
       <div class="flex place-items-center gap-2 text-sm">
         {#if $albumViewSettings.view === AlbumViewMode.List}
-          <ViewGridOutline size="18" />
+          <Icon path={mdiViewGridOutline} size="18" />
           <p class="hidden sm:block">Cover</p>
         {:else}
-          <FormatListBulletedSquare size="18" />
+          <Icon path={mdiFormatListBulletedSquare} size="18" />
           <p class="hidden sm:block">List</p>
         {/if}
       </div>
@@ -293,13 +296,13 @@
                 on:click|stopPropagation={() => handleEdit(album)}
                 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"
               >
-                <PencilOutline size="16" />
+                <Icon path={mdiPencilOutline} size="16" />
               </button>
               <button
                 on:click|stopPropagation={() => chooseAlbumToDelete(album)}
                 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"
               >
-                <TrashCanOutline size="16" />
+                <Icon path={mdiTrashCanOutline} size="16" />
               </button>
             </td>
           </tr>
@@ -323,7 +326,7 @@
   <ContextMenu {...$contextMenuPosition} on:outclick={closeAlbumContextMenu} on:escape={closeAlbumContextMenu}>
     <MenuOption on:click={() => setAlbumToDelete()}>
       <span class="flex place-content-center place-items-center gap-2">
-        <DeleteOutline size="18" />
+        <Icon path={mdiDeleteOutline} size="18" />
         <p>Delete album</p>
       </span>
     </MenuOption>

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

@@ -35,17 +35,20 @@
   import { openFileUploadDialog } from '$lib/utils/file-uploader';
   import { handleError } from '$lib/utils/handle-error';
   import { 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';
-  import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte';
-  import FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte';
-  import Link from 'svelte-material-icons/Link.svelte';
-  import Plus from 'svelte-material-icons/Plus.svelte';
-  import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import type { PageData } from './$types';
   import { clickOutside } from '$lib/utils/click-outside';
   import { getContextMenuPosition } from '$lib/utils/context-menu';
+  import {
+    mdiPlus,
+    mdiDotsVertical,
+    mdiArrowLeft,
+    mdiFileImagePlusOutline,
+    mdiShareVariantOutline,
+    mdiDeleteOutline,
+    mdiFolderDownloadOutline,
+    mdiLink,
+  } from '@mdi/js';
 
   export let data: PageData;
 
@@ -313,11 +316,11 @@
     <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
       <CreateSharedLink />
       <SelectAllAssets {assetStore} {assetInteractionStore} />
-      <AssetSelectContextMenu icon={Plus} title="Add">
+      <AssetSelectContextMenu icon={mdiPlus} title="Add">
         <AddToAlbum />
         <AddToAlbum shared />
       </AssetSelectContextMenu>
-      <AssetSelectContextMenu icon={DotsVertical} title="Menu">
+      <AssetSelectContextMenu icon={mdiDotsVertical} title="Menu">
         {#if isAllUserOwned}
           <FavoriteAction menuItem removeFavorite={isAllFavorite} />
         {/if}
@@ -333,33 +336,33 @@
     </AssetSelectControlBar>
   {:else}
     {#if viewMode === ViewMode.VIEW || viewMode === ViewMode.ALBUM_OPTIONS}
-      <ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => goto(backUrl)}>
+      <ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close-button-click={() => goto(backUrl)}>
         <svelte:fragment slot="trailing">
           <CircleIconButton
             title="Add Photos"
             on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
-            logo={FileImagePlusOutline}
+            icon={mdiFileImagePlusOutline}
           />
 
           {#if isOwned}
             <CircleIconButton
               title="Share"
               on:click={() => (viewMode = ViewMode.SELECT_USERS)}
-              logo={ShareVariantOutline}
+              icon={mdiShareVariantOutline}
             />
             <CircleIconButton
               title="Delete album"
               on:click={() => (viewMode = ViewMode.CONFIRM_DELETE)}
-              logo={DeleteOutline}
+              icon={mdiDeleteOutline}
             />
           {/if}
 
           {#if album.assetCount > 0}
-            <CircleIconButton title="Download" on:click={handleDownloadAlbum} logo={FolderDownloadOutline} />
+            <CircleIconButton title="Download" on:click={handleDownloadAlbum} icon={mdiFolderDownloadOutline} />
 
             {#if isOwned}
               <div use:clickOutside on:outclick={() => (viewMode = ViewMode.VIEW)}>
-                <CircleIconButton title="Album options" on:click={handleOpenAlbumOptions} logo={DotsVertical}>
+                <CircleIconButton title="Album options" on:click={handleOpenAlbumOptions} icon={mdiDotsVertical}>
                   {#if viewMode === ViewMode.ALBUM_OPTIONS}
                     <ContextMenu {...contextMenuPosition}>
                       <MenuOption on:click={() => (viewMode = ViewMode.SELECT_THUMBNAIL)} text="Set album cover" />
@@ -465,7 +468,7 @@
                   backgroundColor="#d3d3d3"
                   forceDark
                   size="20"
-                  logo={Link}
+                  icon={mdiLink}
                   on:click={() => (viewMode = ViewMode.LINK_SHARING)}
                 />
               {/if}
@@ -487,7 +490,7 @@
                   backgroundColor="#d3d3d3"
                   forceDark
                   size="20"
-                  logo={Plus}
+                  icon={mdiPlus}
                   on:click={() => (viewMode = ViewMode.SELECT_USERS)}
                   title="Add more users"
                 />
@@ -518,7 +521,9 @@
               on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
               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="text-text-immich-primary dark:text-immich-dark-primary"><Plus size="24" /> </span>
+              <span class="text-text-immich-primary dark:text-immich-dark-primary"
+                ><Icon path={mdiPlus} size="24" />
+              </span>
               <span class="text-lg">Select photos</span>
             </button>
           </div>

+ 3 - 4
web/src/routes/(user)/archive/+page.svelte

@@ -15,9 +15,8 @@
   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { AssetStore } from '$lib/stores/assets.store';
   import { TimeBucketSize } from '@api';
-  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
-  import Plus from 'svelte-material-icons/Plus.svelte';
   import type { PageData } from './$types';
+  import { mdiPlus, mdiDotsVertical } from '@mdi/js';
 
   export let data: PageData;
 
@@ -33,12 +32,12 @@
     <ArchiveAction unarchive onArchive={(ids) => assetStore.removeAssets(ids)} />
     <CreateSharedLink />
     <SelectAllAssets {assetStore} {assetInteractionStore} />
-    <AssetSelectContextMenu icon={Plus} title="Add">
+    <AssetSelectContextMenu icon={mdiPlus} title="Add">
       <AddToAlbum />
       <AddToAlbum shared />
     </AssetSelectContextMenu>
     <DeleteAssets onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} />
-    <AssetSelectContextMenu icon={DotsVertical} title="Add">
+    <AssetSelectContextMenu icon={mdiDotsVertical} title="Add">
       <DownloadAction menuItem />
       <FavoriteAction menuItem removeFavorite={isAllFavorite} />
     </AssetSelectContextMenu>

+ 13 - 10
web/src/routes/(user)/explore/+page.svelte

@@ -4,12 +4,15 @@
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import { AppRoute } from '$lib/constants';
   import { AssetTypeEnum, SearchExploreResponseDto, api } from '@api';
-  import ClockOutline from 'svelte-material-icons/ClockOutline.svelte';
-  import HeartMultipleOutline from 'svelte-material-icons/HeartMultipleOutline.svelte';
-  import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
-  import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
-  import Rotate360Icon from 'svelte-material-icons/Rotate360.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import type { PageData } from './$types';
+  import {
+    mdiHeartMultipleOutline,
+    mdiClockOutline,
+    mdiPlayCircleOutline,
+    mdiMotionPlayOutline,
+    mdiRotate360,
+  } from '@mdi/js';
 
   export let data: PageData;
 
@@ -118,7 +121,7 @@
           class="flex w-full content-center gap-2 text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary"
           draggable="false"
         >
-          <HeartMultipleOutline size={24} />
+          <Icon path={mdiHeartMultipleOutline} size={24} />
           <span>Favorites</span>
         </a>
         <a
@@ -126,7 +129,7 @@
           class="flex w-full content-center gap-2 text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary"
           draggable="false"
         >
-          <ClockOutline size={24} />
+          <Icon path={mdiClockOutline} size={24} />
           <span>Recently added</span>
         </a>
       </div>
@@ -138,7 +141,7 @@
           href="/search?type={AssetTypeEnum.Video}"
           class="flex w-full items-center gap-2 text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary"
         >
-          <PlayCircleOutline size={24} />
+          <Icon path={mdiPlayCircleOutline} size={24} />
           <span>Videos</span>
         </a>
         <div>
@@ -146,7 +149,7 @@
             href="/search?motion=true"
             class="flex w-full items-center gap-2 text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary"
           >
-            <MotionPlayOutline size={24} />
+            <Icon path={mdiMotionPlayOutline} size={24} />
             <span>Motion photos</span>
           </a>
         </div>
@@ -155,7 +158,7 @@
             href="/search?exifInfo.projectionType=EQUIRECTANGULAR"
             class="flex w-full items-center gap-2 text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary"
           >
-            <Rotate360Icon size={24} />
+            <Icon path={mdiRotate360} size={24} />
             <span>Panorama photos</span>
           </a>
         </div>

+ 3 - 4
web/src/routes/(user)/favorites/+page.svelte

@@ -15,9 +15,8 @@
   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { AssetStore } from '$lib/stores/assets.store';
   import { TimeBucketSize } from '@api';
-  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
-  import Plus from 'svelte-material-icons/Plus.svelte';
   import type { PageData } from './$types';
+  import { mdiDotsVertical, mdiPlus } from '@mdi/js';
 
   export let data: PageData;
 
@@ -34,12 +33,12 @@
     <FavoriteAction removeFavorite onFavorite={(ids) => assetStore.removeAssets(ids)} />
     <CreateSharedLink />
     <SelectAllAssets {assetStore} {assetInteractionStore} />
-    <AssetSelectContextMenu icon={Plus} title="Add">
+    <AssetSelectContextMenu icon={mdiPlus} title="Add">
       <AddToAlbum />
       <AddToAlbum shared />
     </AssetSelectContextMenu>
     <DeleteAssets onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} />
-    <AssetSelectContextMenu icon={DotsVertical} title="Menu">
+    <AssetSelectContextMenu icon={mdiDotsVertical} title="Menu">
       <DownloadAction menuItem />
       <ArchiveAction menuItem unarchive={isAllArchive} />
     </AssetSelectContextMenu>

+ 3 - 2
web/src/routes/(user)/map/+page.svelte

@@ -12,8 +12,9 @@
   import { isEqual, omit } from 'lodash-es';
   import { DateTime, Duration } from 'luxon';
   import { onDestroy, onMount } from 'svelte';
-  import Cog from 'svelte-material-icons/Cog.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import type { PageData } from './$types';
+  import { mdiCog } from '@mdi/js';
 
   export let data: PageData;
 
@@ -135,7 +136,7 @@
               title="Open map settings"
               on:click={() => (showSettingsModal = true)}
             >
-              <Cog size="100%" class="p-1" />
+              <Icon path={mdiCog} size="100%" class="p-1" />
             </button>
           </Control>
         </Map>

+ 3 - 4
web/src/routes/(user)/partners/[userId]/+page.svelte

@@ -12,9 +12,8 @@
   import { AssetStore } from '$lib/stores/assets.store';
   import { TimeBucketSize } from '@api';
   import { onDestroy } from 'svelte';
-  import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
-  import Plus from 'svelte-material-icons/Plus.svelte';
   import type { PageData } from './$types';
+  import { mdiPlus, mdiArrowLeft } from '@mdi/js';
 
   export let data: PageData;
 
@@ -31,14 +30,14 @@
   {#if $isMultiSelectState}
     <AssetSelectControlBar assets={$selectedAssets} clearSelect={assetInteractionStore.clearMultiselect}>
       <CreateSharedLink />
-      <AssetSelectContextMenu icon={Plus} title="Add">
+      <AssetSelectContextMenu icon={mdiPlus} title="Add">
         <AddToAlbum />
         <AddToAlbum shared />
       </AssetSelectContextMenu>
       <DownloadAction />
     </AssetSelectControlBar>
   {:else}
-    <ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => goto(AppRoute.SHARING)}>
+    <ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close-button-click={() => goto(AppRoute.SHARING)}>
       <svelte:fragment slot="leading">
         <p class="whitespace-nowrap text-immich-fg dark:text-immich-dark-fg">
           {data.partner.firstName}

+ 5 - 5
web/src/routes/(user)/people/+page.svelte

@@ -1,6 +1,5 @@
 <script lang="ts">
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
-  import AccountOff from 'svelte-material-icons/AccountOff.svelte';
   import type { PageData } from './$types';
   import PeopleCard from '$lib/components/faces-page/people-card.svelte';
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
@@ -15,19 +14,20 @@
   } from '$lib/components/shared-components/notification/notification';
   import ShowHide from '$lib/components/faces-page/show-hide.svelte';
   import IconButton from '$lib/components/elements/buttons/icon-button.svelte';
-  import EyeOutline from 'svelte-material-icons/EyeOutline.svelte';
   import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
   import { onDestroy, onMount } from 'svelte';
   import { browser } from '$app/environment';
   import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte';
   import SetBirthDateModal from '$lib/components/faces-page/set-birth-date-modal.svelte';
   import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
+  import { mdiAccountOff, mdiEyeOutline } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   export let data: PageData;
   let selectHidden = false;
   let initialHiddenValues: Record<string, boolean> = {};
 
-  let eyeColorMap: Record<string, string> = {};
+  let eyeColorMap: Record<string, 'black' | 'white'> = {};
 
   let people = data.people.people;
   let countTotalPeople = data.people.total;
@@ -362,7 +362,7 @@
     {#if countTotalPeople > 0}
       <IconButton on:click={() => (selectHidden = !selectHidden)}>
         <div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
-          <EyeOutline size="18" />
+          <Icon path={mdiEyeOutline} size="18" />
           <p class="ml-2">Show & hide faces</p>
         </div>
       </IconButton>
@@ -388,7 +388,7 @@
   {:else}
     <div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
       <div class="flex flex-col content-center items-center text-center">
-        <AccountOff size="3.5em" />
+        <Icon path={mdiAccountOff} size="3.5em" />
         <p class="mt-5 text-3xl font-medium">No people</p>
       </div>
     </div>

+ 5 - 7
web/src/routes/(user)/people/[personId]/+page.svelte

@@ -29,13 +29,11 @@
   import { handleError } from '$lib/utils/handle-error';
   import { AssetResponseDto, PersonResponseDto, TimeBucketSize, api } from '@api';
   import { onMount } from 'svelte';
-  import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
-  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
-  import Plus from 'svelte-material-icons/Plus.svelte';
   import type { PageData } from './$types';
   import { clickOutside } from '$lib/utils/click-outside';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
+  import { mdiPlus, mdiDotsVertical, mdiArrowLeft } from '@mdi/js';
 
   export let data: PageData;
 
@@ -370,12 +368,12 @@
     <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
       <CreateSharedLink />
       <SelectAllAssets {assetStore} {assetInteractionStore} />
-      <AssetSelectContextMenu icon={Plus} title="Add">
+      <AssetSelectContextMenu icon={mdiPlus} title="Add">
         <AddToAlbum />
         <AddToAlbum shared />
       </AssetSelectContextMenu>
       <DeleteAssets onAssetDelete={(assetId) => $assetStore.removeAsset(assetId)} />
-      <AssetSelectContextMenu icon={DotsVertical} title="Add">
+      <AssetSelectContextMenu icon={mdiDotsVertical} title="Add">
         <DownloadAction menuItem filename="{data.person.name || 'immich'}.zip" />
         <FavoriteAction menuItem removeFavorite={isAllFavorite} />
         <ArchiveAction menuItem unarchive={isAllArchive} onArchive={(ids) => $assetStore.removeAssets(ids)} />
@@ -383,9 +381,9 @@
     </AssetSelectControlBar>
   {:else}
     {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE}
-      <ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => goto(previousRoute)}>
+      <ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close-button-click={() => goto(previousRoute)}>
         <svelte:fragment slot="trailing">
-          <AssetSelectContextMenu icon={DotsVertical} title="Menu">
+          <AssetSelectContextMenu icon={mdiDotsVertical} title="Menu">
             <MenuOption text="Change feature photo" on:click={() => (viewMode = ViewMode.SELECT_FACE)} />
             <MenuOption text="Set date of birth" on:click={() => (viewMode = ViewMode.BIRTH_DATE)} />
             <MenuOption text="Merge face" on:click={() => (viewMode = ViewMode.MERGE_FACES)} />

+ 3 - 4
web/src/routes/(user)/photos/+page.svelte

@@ -18,10 +18,9 @@
   import { AssetStore } from '$lib/stores/assets.store';
   import { openFileUploadDialog } from '$lib/utils/file-uploader';
   import { TimeBucketSize } from '@api';
-  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
-  import Plus from 'svelte-material-icons/Plus.svelte';
   import type { PageData } from './$types';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
+  import { mdiDotsVertical, mdiPlus } from '@mdi/js';
 
   export let data: PageData;
 
@@ -52,7 +51,7 @@
   <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
     <CreateSharedLink on:escape={() => (handleEscapeKey = true)} />
     <SelectAllAssets {assetStore} {assetInteractionStore} />
-    <AssetSelectContextMenu icon={Plus} title="Add">
+    <AssetSelectContextMenu icon={mdiPlus} title="Add">
       <AddToAlbum />
       <AddToAlbum shared />
     </AssetSelectContextMenu>
@@ -60,7 +59,7 @@
       on:escape={() => (handleEscapeKey = true)}
       onAssetDelete={(assetId) => assetStore.removeAsset(assetId)}
     />
-    <AssetSelectContextMenu icon={DotsVertical} title="Menu">
+    <AssetSelectContextMenu icon={mdiDotsVertical} title="Menu">
       <FavoriteAction menuItem removeFavorite={isAllFavorite} />
       <DownloadAction menuItem />
       <ArchiveAction menuItem onArchive={(ids) => assetStore.removeAssets(ids)} />

+ 7 - 10
web/src/routes/(user)/search/+page.svelte

@@ -13,12 +13,8 @@
   import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
   import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
   import type { AssetResponseDto } from '@api';
-  import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
-  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
-  import ImageOffOutline from 'svelte-material-icons/ImageOffOutline.svelte';
-  import Plus from 'svelte-material-icons/Plus.svelte';
   import type { PageData } from './$types';
-  import SelectAll from 'svelte-material-icons/SelectAll.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import { AppRoute } from '$lib/constants';
   import AlbumCard from '$lib/components/album-page/album-card.svelte';
@@ -28,6 +24,7 @@
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import { preventRaceConditionSearchBar } from '$lib/stores/search.store';
   import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
+  import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
 
   export let data: PageData;
 
@@ -109,21 +106,21 @@
   {#if isMultiSelectionMode}
     <AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
       <CreateSharedLink />
-      <CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
-      <AssetSelectContextMenu icon={Plus} title="Add">
+      <CircleIconButton title="Select all" icon={mdiSelectAll} on:click={handleSelectAll} />
+      <AssetSelectContextMenu icon={mdiPlus} title="Add">
         <AddToAlbum />
         <AddToAlbum shared />
       </AssetSelectContextMenu>
       <DeleteAssets {onAssetDelete} />
 
-      <AssetSelectContextMenu icon={DotsVertical} title="Add">
+      <AssetSelectContextMenu icon={mdiDotsVertical} title="Add">
         <DownloadAction menuItem />
         <FavoriteAction menuItem removeFavorite={isAllFavorite} />
         <ArchiveAction menuItem unarchive={isAllArchived} />
       </AssetSelectContextMenu>
     </AssetSelectControlBar>
   {:else}
-    <ControlAppBar on:close-button-click={() => goto(previousRoute)} backIcon={ArrowLeft}>
+    <ControlAppBar on:close-button-click={() => goto(previousRoute)} backIcon={mdiArrowLeft}>
       <div class="w-full flex-1 pl-4">
         <SearchBar grayTheme={false} value={term} />
       </div>
@@ -155,7 +152,7 @@
       {:else}
         <div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
           <div class="flex flex-col content-center items-center text-center">
-            <ImageOffOutline size="3.5em" />
+            <Icon path={mdiImageOffOutline} size="3.5em" />
             <p class="mt-5 text-3xl font-medium">No results</p>
             <p class="text-base font-normal">Try a synonym or more general keyword</p>
           </div>

+ 4 - 4
web/src/routes/(user)/sharing/+page.svelte

@@ -12,10 +12,10 @@
   import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
   import { AppRoute } from '$lib/constants';
   import { api } from '@api';
-  import Link from 'svelte-material-icons/Link.svelte';
-  import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
   import { flip } from 'svelte/animate';
   import type { PageData } from './$types';
+  import { mdiLink, mdiPlusBoxOutline } from '@mdi/js';
+  import Icon from '$lib/components/elements/icon.svelte';
 
   export let data: PageData;
 
@@ -43,14 +43,14 @@
   <div class="flex" slot="buttons">
     <LinkButton on:click={createSharedAlbum}>
       <div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
-        <PlusBoxOutline size="18" class="shrink-0" />
+        <Icon path={mdiPlusBoxOutline} size="18" class="shrink-0" />
         <span class="leading-none max-sm:text-xs">Create shared album</span>
       </div>
     </LinkButton>
 
     <LinkButton on:click={() => goto(AppRoute.SHARED_LINKS)}>
       <div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
-        <Link size="18" class="shrink-0" />
+        <Icon path={mdiLink} size="18" class="shrink-0" />
         <span class="leading-none max-sm:text-xs">Shared links</span>
       </div>
     </LinkButton>

+ 2 - 2
web/src/routes/(user)/sharing/sharedlinks/+page.svelte

@@ -1,6 +1,5 @@
 <script lang="ts">
   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
-  import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
   import { api, copyToClipboard, SharedLinkResponseDto } from '@api';
   import { goto } from '$app/navigation';
   import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
@@ -13,6 +12,7 @@
   import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
   import { handleError } from '$lib/utils/handle-error';
   import { AppRoute } from '$lib/constants';
+  import { mdiArrowLeft } from '@mdi/js';
 
   let sharedLinks: SharedLinkResponseDto[] = [];
   let editSharedLink: SharedLinkResponseDto | null = null;
@@ -53,7 +53,7 @@
   };
 </script>
 
-<ControlAppBar backIcon={ArrowLeft} on:close-button-click={() => goto(AppRoute.SHARING)}>
+<ControlAppBar backIcon={mdiArrowLeft} on:close-button-click={() => goto(AppRoute.SHARING)}>
   <svelte:fragment slot="leading">Shared links</svelte:fragment>
 </ControlAppBar>
 

+ 4 - 4
web/src/routes/(user)/trash/+page.svelte

@@ -16,13 +16,13 @@
   import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
   import { AssetStore } from '$lib/stores/assets.store';
   import { api, TimeBucketSize } from '@api';
-  import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
-  import HistoryOutline from 'svelte-material-icons/History.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import type { PageData } from './$types';
   import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
   import { goto } from '$app/navigation';
   import empty3Url from '$lib/assets/empty-3.svg';
   import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
+  import { mdiDeleteOutline, mdiHistory } from '@mdi/js';
 
   export let data: PageData;
 
@@ -74,13 +74,13 @@
     <div class="flex place-items-center gap-2" slot="buttons">
       <LinkButton on:click={handleRestoreTrash}>
         <div class="flex place-items-center gap-2 text-sm">
-          <HistoryOutline size="18" />
+          <Icon path={mdiHistory} size="18" />
           Restore All
         </div>
       </LinkButton>
       <LinkButton on:click={() => (isShowEmptyConfirmation = true)}>
         <div class="flex place-items-center gap-2 text-sm">
-          <DeleteOutline size="18" />
+          <Icon path={mdiDeleteOutline} size="18" />
           Empty Trash
         </div>
       </LinkButton>

+ 6 - 8
web/src/routes/+error.svelte

@@ -1,10 +1,8 @@
 <script>
   import { page } from '$app/stores';
+  import Icon from '$lib/components/elements/icon.svelte';
   import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
-  import CodeTags from 'svelte-material-icons/CodeTags.svelte';
-  import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
-  import Message from 'svelte-material-icons/Message.svelte';
-  import PartyPopper from 'svelte-material-icons/PartyPopper.svelte';
+  import { mdiCodeTags, mdiContentCopy, mdiMessage, mdiPartyPopper } from '@mdi/js';
   import { copyToClipboard } from '../api/utils';
 
   const handleCopy = async () => {
@@ -43,7 +41,7 @@
                 on:click={() => handleCopy()}
                 class="rounded-full bg-immich-primary px-3 py-2 text-sm text-white shadow-md transition-colors hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-immich-dark-gray dark:hover:bg-immich-dark-primary/80"
               >
-                <ContentCopy size={24} />
+                <Icon path={mdiContentCopy} size={24} />
               </button>
             </div>
           </div>
@@ -71,7 +69,7 @@
               class="flex grow basis-0 justify-center p-4"
             >
               <button class="flex flex-col place-content-center place-items-center gap-2">
-                <Message size={24} />
+                <Icon path={mdiMessage} size={24} />
                 <p class="text-sm">Get Help</p>
               </button>
             </a>
@@ -83,7 +81,7 @@
               class="flex grow basis-0 justify-center p-4"
             >
               <button class="flex flex-col place-content-center place-items-center gap-2">
-                <PartyPopper size={24} />
+                <Icon path={mdiPartyPopper} size={24} />
                 <p class="text-sm">Read Changelog</p>
               </button>
             </a>
@@ -95,7 +93,7 @@
               class="flex grow basis-0 justify-center p-4"
             >
               <button class="flex flex-col place-content-center place-items-center gap-2">
-                <CodeTags size={24} />
+                <Icon path={mdiCodeTags} size={24} />
                 <p class="text-sm">Check Logs</p>
               </button>
             </a>

+ 3 - 2
web/src/routes/admin/jobs-status/+page.svelte

@@ -5,8 +5,9 @@
   import { AppRoute } from '$lib/constants';
   import { AllJobStatusResponseDto, api } from '@api';
   import { onDestroy, onMount } from 'svelte';
-  import CogIcon from 'svelte-material-icons/Cog.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import type { PageData } from './$types';
+  import { mdiCog } from '@mdi/js';
 
   export let data: PageData;
 
@@ -34,7 +35,7 @@
     <a href="{AppRoute.ADMIN_SETTINGS}?open=job-settings">
       <LinkButton>
         <div class="flex place-items-center gap-2 text-sm">
-          <CogIcon size="18" />
+          <Icon path={mdiCog} size="18" />
           Manage Concurrency
         </div>
       </LinkButton>

+ 8 - 11
web/src/routes/admin/repair/+page.svelte

@@ -12,12 +12,9 @@
   import { downloadBlob } from '$lib/utils/asset-utils';
   import { handleError } from '$lib/utils/handle-error';
   import { FileReportItemDto, api, copyToClipboard } from '@api';
-  import CheckAll from 'svelte-material-icons/CheckAll.svelte';
-  import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
-  import Download from 'svelte-material-icons/Download.svelte';
-  import Refresh from 'svelte-material-icons/Refresh.svelte';
-  import Wrench from 'svelte-material-icons/Wrench.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import type { PageData } from './$types';
+  import { mdiWrench, mdiCheckAll, mdiDownload, mdiRefresh, mdiContentCopy } from '@mdi/js';
 
   export let data: PageData;
 
@@ -178,25 +175,25 @@
   <div class="flex justify-end gap-2" slot="buttons">
     <LinkButton on:click={() => handleRepair()} disabled={matches.length === 0 || repairing}>
       <div class="flex place-items-center gap-2 text-sm">
-        <Wrench size="18" />
+        <Icon path={mdiWrench} size="18" />
         Repair All
       </div>
     </LinkButton>
     <LinkButton on:click={() => handleCheckAll()} disabled={extras.length === 0 || checking}>
       <div class="flex place-items-center gap-2 text-sm">
-        <CheckAll size="18" />
+        <Icon path={mdiCheckAll} size="18" />
         Check All
       </div>
     </LinkButton>
     <LinkButton on:click={() => handleDownload()} disabled={extras.length + orphans.length === 0}>
       <div class="flex place-items-center gap-2 text-sm">
-        <Download size="18" />
+        <Icon path={mdiDownload} size="18" />
         Export
       </div>
     </LinkButton>
     <LinkButton on:click={() => handleRefresh()}>
       <div class="flex place-items-center gap-2 text-sm">
-        <Refresh size="18" />
+        <Icon path={mdiRefresh} size="18" />
         Refresh
       </div>
     </LinkButton>
@@ -273,7 +270,7 @@
                   title={orphan.pathValue}
                 >
                   <td on:click={() => copyToClipboard(orphan.pathValue)}>
-                    <CircleIconButton logo={ContentCopy} size="18" />
+                    <CircleIconButton icon={mdiContentCopy} size="18" />
                   </td>
                   <td class="truncate text-sm font-mono text-left" title={orphan.pathValue}>
                     {orphan.pathValue}
@@ -313,7 +310,7 @@
                   title={extra.filename}
                 >
                   <td on:click={() => copyToClipboard(extra.filename)}>
-                    <CircleIconButton logo={ContentCopy} size="18" />
+                    <CircleIconButton icon={mdiContentCopy} size="18" />
                   </td>
                   <td class="w-full text-md text-ellipsis flex justify-between pr-5">
                     <span class="text-ellipsis grow truncate font-mono text-sm pr-5" title={extra.filename}

+ 5 - 6
web/src/routes/admin/system-settings/+page.svelte

@@ -17,11 +17,10 @@
   import { featureFlags } from '$lib/stores/server-config.store';
   import { downloadBlob } from '$lib/utils/asset-utils';
   import { copyToClipboard } from '@api';
-  import Alert from 'svelte-material-icons/Alert.svelte';
-  import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
-  import Download from 'svelte-material-icons/Download.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import type { PageData } from './$types';
   import NewVersionCheckSettings from '$lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte';
+  import { mdiAlert, mdiContentCopy, mdiDownload } from '@mdi/js';
 
   export let data: PageData;
 
@@ -39,7 +38,7 @@
 
 {#if $featureFlags.configFile}
   <div class="mb-8 flex flex-row items-center gap-2 rounded-md bg-gray-100 p-3 dark:bg-gray-800">
-    <Alert class="text-yellow-400" size={18} />
+    <Icon path={mdiAlert} class="text-yellow-400" size={18} />
     <h2 class="text-md text-immich-primary dark:text-immich-dark-primary">Config is currently set by a config file</h2>
   </div>
 {/if}
@@ -48,13 +47,13 @@
   <div class="flex justify-end gap-2" slot="buttons">
     <LinkButton on:click={() => copyToClipboard(JSON.stringify(configs, null, 2))}>
       <div class="flex place-items-center gap-2 text-sm">
-        <ContentCopy size="18" />
+        <Icon path={mdiContentCopy} size="18" />
         Copy to Clipboard
       </div>
     </LinkButton>
     <LinkButton on:click={() => downloadConfig()}>
       <div class="flex place-items-center gap-2 text-sm">
-        <Download size="18" />
+        <Icon path={mdiDownload} size="18" />
         Export as JSON
       </div>
     </LinkButton>

+ 10 - 14
web/src/routes/admin/user-management/+page.svelte

@@ -1,12 +1,7 @@
 <script lang="ts">
   import { api, UserResponseDto } from '@api';
-
   import { onMount } from 'svelte';
-  import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
-  import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
-  import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte';
-  import Check from 'svelte-material-icons/Check.svelte';
-  import Close from 'svelte-material-icons/Close.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
   import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
@@ -17,6 +12,7 @@
   import Button from '$lib/components/elements/buttons/button.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import type { PageData } from './$types';
+  import { mdiCheck, mdiClose, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
 
   export let data: PageData;
 
@@ -196,9 +192,9 @@
                 <td class="w-2/12 text-ellipsis break-all px-2 text-sm">
                   <div class="container mx-auto flex flex-wrap justify-center">
                     {#if user.externalPath}
-                      <Check size="16" />
+                      <Icon path={mdiCheck} size="16" />
                     {:else}
-                      <Close size="16" />
+                      <Icon path={mdiClose} size="16" />
                     {/if}
                   </div>
                 </td>
@@ -208,14 +204,14 @@
                       on:click={() => editUserHandler(user)}
                       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"
                     >
-                      <PencilOutline size="16" />
+                      <Icon path={mdiPencilOutline} size="16" />
                     </button>
                     {#if user.id !== data.user.id}
                       <button
                         on:click={() => deleteUserHandler(user)}
                         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"
                       >
-                        <TrashCanOutline size="16" />
+                        <Icon path={mdiTrashCanOutline} size="16" />
                       </button>
                     {/if}
                   {/if}
@@ -225,7 +221,7 @@
                       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"
                       title={`scheduled removal on ${getDeleteDate(user)}`}
                     >
-                      <DeleteRestore size="16" />
+                      <Icon path={mdiDeleteRestore} size="16" />
                     </button>
                   {/if}
                 </td>
@@ -265,13 +261,13 @@
                       on:click={() => editUserHandler(user)}
                       class="rounded-full bg-immich-primary p-2 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700 max-sm:mb-1 sm:p-3"
                     >
-                      <PencilOutline size="16" />
+                      <Icon path={mdiPencilOutline} size="16" />
                     </button>
                     <button
                       on:click={() => deleteUserHandler(user)}
                       class="rounded-full bg-immich-primary p-2 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700 sm:p-3"
                     >
-                      <TrashCanOutline size="16" />
+                      <Icon path={mdiTrashCanOutline} size="16" />
                     </button>
                   {/if}
                   {#if isDeleted(user)}
@@ -280,7 +276,7 @@
                       class="rounded-full bg-immich-primary p-2 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700 sm:p-3"
                       title={`scheduled removal on ${getDeleteDate(user)}`}
                     >
-                      <DeleteRestore size="16" />
+                      <Icon path={mdiDeleteRestore} size="16" />
                     </button>
                   {/if}
                 </td>