diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte
index 0696a937cc..befe2019a1 100644
--- a/web/src/lib/components/album-page/albums-list.svelte
+++ b/web/src/lib/components/album-page/albums-list.svelte
@@ -1,43 +1,47 @@
 <script lang="ts">
-  import { onMount, type Snippet } from 'svelte';
-  import { groupBy } from 'lodash-es';
-  import { addUsersToAlbum, deleteAlbum, type AlbumUserAddDto, type AlbumResponseDto, isHttpError } from '@immich/sdk';
-  import { mdiDeleteOutline, mdiShareVariantOutline, mdiFolderDownloadOutline, mdiRenameOutline } from '@mdi/js';
+  import { goto } from '$app/navigation';
+  import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
+  import AlbumsTable from '$lib/components/album-page/albums-table.svelte';
   import EditAlbumForm from '$lib/components/forms/edit-album-form.svelte';
-  import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
+  import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
+  import RightClickContextMenu from '$lib/components/shared-components/context-menu/right-click-context-menu.svelte';
   import {
     NotificationType,
     notificationController,
   } from '$lib/components/shared-components/notification/notification';
-  import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
-  import RightClickContextMenu from '$lib/components/shared-components/context-menu/right-click-context-menu.svelte';
-  import AlbumsTable from '$lib/components/album-page/albums-table.svelte';
-  import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
-  import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte';
-  import { handleError } from '$lib/utils/handle-error';
-  import { downloadAlbum } from '$lib/utils/asset-utils';
-  import { normalizeSearchString } from '$lib/utils/string-utils';
-  import {
-    getSelectedAlbumGroupOption,
-    type AlbumGroup,
-    confirmAlbumDelete,
-    sortAlbums,
-    stringToSortOrder,
-  } from '$lib/utils/album-utils';
-  import type { ContextMenuPosition } from '$lib/utils/context-menu';
-  import { user } from '$lib/stores/user.store';
+  import { AppRoute } from '$lib/constants';
+  import { modalManager } from '$lib/managers/modal-manager.svelte';
+  import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
+  import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
+  import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
   import {
+    AlbumFilter,
     AlbumGroupBy,
     AlbumSortBy,
-    AlbumFilter,
     AlbumViewMode,
     SortOrder,
     locale,
     type AlbumViewSettings,
   } from '$lib/stores/preferences.store';
+  import { serverConfig } from '$lib/stores/server-config.store';
+  import { user } from '$lib/stores/user.store';
   import { userInteraction } from '$lib/stores/user.svelte';
-  import { goto } from '$app/navigation';
-  import { AppRoute } from '$lib/constants';
+  import { makeSharedLinkUrl } from '$lib/utils';
+  import {
+    confirmAlbumDelete,
+    getSelectedAlbumGroupOption,
+    sortAlbums,
+    stringToSortOrder,
+    type AlbumGroup,
+  } from '$lib/utils/album-utils';
+  import { downloadAlbum } from '$lib/utils/asset-utils';
+  import type { ContextMenuPosition } from '$lib/utils/context-menu';
+  import { handleError } from '$lib/utils/handle-error';
+  import { normalizeSearchString } from '$lib/utils/string-utils';
+  import { addUsersToAlbum, deleteAlbum, isHttpError, type AlbumResponseDto, type AlbumUserAddDto } from '@immich/sdk';
+  import { mdiDeleteOutline, mdiFolderDownloadOutline, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js';
+  import { groupBy } from 'lodash-es';
+  import { onMount, type Snippet } from 'svelte';
   import { t } from 'svelte-i18n';
   import { run } from 'svelte/legacy';
 
@@ -140,8 +144,6 @@
 
   let albumGroupOption: string = $state(AlbumGroupBy.None);
 
-  let showShareByURLModal = $state(false);
-
   let albumToEdit: AlbumResponseDto | null = $state(null);
   let albumToShare: AlbumResponseDto | null = $state(null);
   let albumToDelete: AlbumResponseDto | null = null;
@@ -346,18 +348,32 @@
     updateAlbumInfo(album);
   };
 
-  const openShareModal = () => {
+  const openShareModal = async () => {
     if (!contextMenuTargetAlbum) {
       return;
     }
 
     albumToShare = contextMenuTargetAlbum;
     closeAlbumContextMenu();
-  };
+    const result = await modalManager.show(AlbumShareModal, { album: albumToShare });
 
-  const closeShareModal = () => {
-    albumToShare = null;
-    showShareByURLModal = false;
+    switch (result?.action) {
+      case 'sharedUsers': {
+        await handleAddUsers(result.data);
+        return;
+      }
+
+      case 'sharedLink': {
+        const sharedLink = await modalManager.show(SharedLinkCreateModal, { albumId: albumToShare.id });
+
+        if (sharedLink) {
+          const url = makeSharedLinkUrl($serverConfig.externalDomain, sharedLink.key);
+          handleSharedLinkCreated(albumToShare);
+          await modalManager.show(QrCodeModal, { title: $t('view_link'), value: url });
+        }
+        return;
+      }
+    }
   };
 </script>
 
@@ -419,22 +435,4 @@
       onClose={() => (albumToEdit = null)}
     />
   {/if}
-
-  <!-- Share Modal -->
-  {#if albumToShare}
-    {#if showShareByURLModal}
-      <CreateSharedLinkModal
-        albumId={albumToShare.id}
-        onClose={() => closeShareModal()}
-        onCreated={() => albumToShare && handleSharedLinkCreated(albumToShare)}
-      />
-    {:else}
-      <UserSelectionModal
-        album={albumToShare}
-        onSelect={handleAddUsers}
-        onShare={() => (showShareByURLModal = true)}
-        onClose={() => closeShareModal()}
-      />
-    {/if}
-  {/if}
 {/if}
diff --git a/web/src/lib/components/asset-viewer/actions/share-action.svelte b/web/src/lib/components/asset-viewer/actions/share-action.svelte
index 6fd5aa456e..f32d3e7515 100644
--- a/web/src/lib/components/asset-viewer/actions/share-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/share-action.svelte
@@ -1,7 +1,10 @@
 <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 Portal from '$lib/components/shared-components/portal/portal.svelte';
+  import { modalManager } from '$lib/managers/modal-manager.svelte';
+  import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
+  import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
+  import { serverConfig } from '$lib/stores/server-config.store';
+  import { makeSharedLinkUrl } from '$lib/utils';
   import type { AssetResponseDto } from '@immich/sdk';
   import { mdiShareVariantOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
@@ -12,13 +15,14 @@
 
   let { asset }: Props = $props();
 
-  let showModal = $state(false);
+  const handleClick = async () => {
+    const sharedLink = await modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] });
+
+    if (sharedLink) {
+      const url = makeSharedLinkUrl($serverConfig.externalDomain, sharedLink.key);
+      await modalManager.show(QrCodeModal, { title: $t('view_link'), value: url });
+    }
+  };
 </script>
 
-<CircleIconButton color="opaque" icon={mdiShareVariantOutline} onclick={() => (showModal = true)} title={$t('share')} />
-
-{#if showModal}
-  <Portal target="body">
-    <CreateSharedLinkModal assetIds={[asset.id]} onClose={() => (showModal = false)} />
-  </Portal>
-{/if}
+<CircleIconButton color="opaque" icon={mdiShareVariantOutline} onclick={handleClick} title={$t('share')} />
diff --git a/web/src/lib/components/photos-page/actions/create-shared-link.svelte b/web/src/lib/components/photos-page/actions/create-shared-link.svelte
index 1b99627ea9..05baf822c1 100644
--- a/web/src/lib/components/photos-page/actions/create-shared-link.svelte
+++ b/web/src/lib/components/photos-page/actions/create-shared-link.svelte
@@ -1,16 +1,26 @@
 <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 { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
+  import { modalManager } from '$lib/managers/modal-manager.svelte';
+  import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
+  import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
+  import { serverConfig } from '$lib/stores/server-config.store';
+  import { makeSharedLinkUrl } from '$lib/utils';
   import { mdiShareVariantOutline } from '@mdi/js';
-  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
   import { t } from 'svelte-i18n';
 
-  let showModal = $state(false);
   const { getAssets } = getAssetControlContext();
+
+  const handleClick = async () => {
+    const sharedLink = await modalManager.show(SharedLinkCreateModal, {
+      assetIds: [...getAssets()].map(({ id }) => id),
+    });
+
+    if (sharedLink) {
+      const url = makeSharedLinkUrl($serverConfig.externalDomain, sharedLink.key);
+      await modalManager.show(QrCodeModal, { title: $t('view_link'), value: url });
+    }
+  };
 </script>
 
-<CircleIconButton title={$t('share')} icon={mdiShareVariantOutline} onclick={() => (showModal = true)} />
-
-{#if showModal}
-  <CreateSharedLinkModal assetIds={[...getAssets()].map(({ id }) => id)} onClose={() => (showModal = false)} />
-{/if}
+<CircleIconButton title={$t('share')} icon={mdiShareVariantOutline} onclick={handleClick} />
diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
deleted file mode 100644
index a87ca3da4a..0000000000
--- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
+++ /dev/null
@@ -1,251 +0,0 @@
-<script lang="ts">
-  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
-  import QrCodeModal from '$lib/components/shared-components/qr-code-modal.svelte';
-  import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
-  import { SettingInputFieldType } from '$lib/constants';
-  import { locale } from '$lib/stores/preferences.store';
-  import { serverConfig } from '$lib/stores/server-config.store';
-  import { makeSharedLinkUrl } from '$lib/utils';
-  import { handleError } from '$lib/utils/handle-error';
-  import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
-  import { Button } from '@immich/ui';
-  import { mdiLink } from '@mdi/js';
-  import { DateTime, Duration } from 'luxon';
-  import { t } from 'svelte-i18n';
-  import { NotificationType, notificationController } from '../notification/notification';
-  import SettingInputField from '../settings/setting-input-field.svelte';
-  import SettingSwitch from '../settings/setting-switch.svelte';
-
-  interface Props {
-    onClose: () => void;
-    albumId?: string | undefined;
-    assetIds?: string[];
-    editingLink?: SharedLinkResponseDto | undefined;
-    onCreated?: () => void;
-  }
-
-  let {
-    onClose,
-    albumId = $bindable(undefined),
-    assetIds = $bindable([]),
-    editingLink = undefined,
-    onCreated = () => {},
-  }: Props = $props();
-
-  let sharedLink: string | null = $state(null);
-  let description = $state('');
-  let allowDownload = $state(true);
-  let allowUpload = $state(false);
-  let showMetadata = $state(true);
-  let expirationOption: number = $state(0);
-  let password = $state('');
-  let shouldChangeExpirationTime = $state(false);
-  let enablePassword = $state(false);
-
-  const expirationOptions: [number, Intl.RelativeTimeFormatUnit][] = [
-    [30, 'minutes'],
-    [1, 'hour'],
-    [6, 'hours'],
-    [1, 'day'],
-    [7, 'days'],
-    [30, 'days'],
-    [3, 'months'],
-    [1, 'year'],
-  ];
-
-  let relativeTime = $derived(new Intl.RelativeTimeFormat($locale));
-  let expiredDateOptions = $derived([
-    { text: $t('never'), value: 0 },
-    ...expirationOptions.map(([value, unit]) => ({
-      text: relativeTime.format(value, unit),
-      value: Duration.fromObject({ [unit]: value }).toMillis(),
-    })),
-  ]);
-
-  let shareType = $derived(albumId ? SharedLinkType.Album : SharedLinkType.Individual);
-
-  $effect(() => {
-    if (!showMetadata) {
-      allowDownload = false;
-    }
-  });
-
-  if (editingLink) {
-    if (editingLink.description) {
-      description = editingLink.description;
-    }
-    if (editingLink.password) {
-      password = editingLink.password;
-    }
-    allowUpload = editingLink.allowUpload;
-    allowDownload = editingLink.allowDownload;
-    showMetadata = editingLink.showMetadata;
-
-    albumId = editingLink.album?.id;
-    assetIds = editingLink.assets.map(({ id }) => id);
-
-    enablePassword = !!editingLink.password;
-  }
-
-  const handleCreateSharedLink = async () => {
-    const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : undefined;
-
-    try {
-      const data = await createSharedLink({
-        sharedLinkCreateDto: {
-          type: shareType,
-          albumId,
-          assetIds,
-          expiresAt: expirationDate,
-          allowUpload,
-          description,
-          password,
-          allowDownload,
-          showMetadata,
-        },
-      });
-      sharedLink = makeSharedLinkUrl($serverConfig.externalDomain, data.key);
-      onCreated();
-    } catch (error) {
-      handleError(error, $t('errors.failed_to_create_shared_link'));
-    }
-  };
-
-  const handleEditLink = async () => {
-    if (!editingLink) {
-      return;
-    }
-
-    try {
-      const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : null;
-
-      await updateSharedLink({
-        id: editingLink.id,
-        sharedLinkEditDto: {
-          description,
-          password: enablePassword ? password : '',
-          expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
-          allowUpload,
-          allowDownload,
-          showMetadata,
-        },
-      });
-
-      notificationController.show({
-        type: NotificationType.Info,
-        message: $t('edited'),
-      });
-
-      onClose();
-    } catch (error) {
-      handleError(error, $t('errors.failed_to_edit_shared_link'));
-    }
-  };
-
-  const getTitle = () => {
-    if (sharedLink) {
-      return $t('view_link');
-    }
-    if (editingLink) {
-      return $t('edit_link');
-    }
-    return $t('create_link_to_share');
-  };
-</script>
-
-{#if !sharedLink || editingLink}
-  <FullScreenModal title={getTitle()} icon={mdiLink} {onClose}>
-    <section>
-      {#if shareType === SharedLinkType.Album}
-        {#if !editingLink}
-          <div>{$t('album_with_link_access')}</div>
-        {:else}
-          <div class="text-sm">
-            {$t('public_album')} |
-            <span class="text-immich-primary dark:text-immich-dark-primary">{editingLink.album?.albumName}</span>
-          </div>
-        {/if}
-      {/if}
-
-      {#if shareType === SharedLinkType.Individual}
-        {#if !editingLink}
-          <div>{$t('create_link_to_share_description')}</div>
-        {:else}
-          <div class="text-sm">
-            {$t('individual_share')} |
-            <span class="text-immich-primary dark:text-immich-dark-primary">{editingLink.description || ''}</span>
-          </div>
-        {/if}
-      {/if}
-
-      <div class="mb-2 mt-4">
-        <p class="text-xs">{$t('link_options').toUpperCase()}</p>
-      </div>
-      <div class="rounded-lg bg-gray-100 p-4 dark:bg-black/40 overflow-y-auto">
-        <div class="flex flex-col">
-          <div class="mb-2">
-            <SettingInputField
-              inputType={SettingInputFieldType.TEXT}
-              label={$t('description')}
-              bind:value={description}
-            />
-          </div>
-
-          <div class="mb-2">
-            <SettingInputField
-              inputType={SettingInputFieldType.TEXT}
-              label={$t('password')}
-              bind:value={password}
-              disabled={!enablePassword}
-            />
-          </div>
-
-          <div class="my-3">
-            <SettingSwitch bind:checked={enablePassword} title={$t('require_password')} />
-          </div>
-
-          <div class="my-3">
-            <SettingSwitch bind:checked={showMetadata} title={$t('show_metadata')} />
-          </div>
-
-          <div class="my-3">
-            <SettingSwitch
-              bind:checked={allowDownload}
-              title={$t('allow_public_user_to_download')}
-              disabled={!showMetadata}
-            />
-          </div>
-
-          <div class="my-3">
-            <SettingSwitch bind:checked={allowUpload} title={$t('allow_public_user_to_upload')} />
-          </div>
-
-          {#if editingLink}
-            <div class="my-3">
-              <SettingSwitch bind:checked={shouldChangeExpirationTime} title={$t('change_expiration_time')} />
-            </div>
-          {/if}
-          <div class="mt-3">
-            <SettingSelect
-              bind:value={expirationOption}
-              options={expiredDateOptions}
-              label={$t('expire_after')}
-              disabled={editingLink && !shouldChangeExpirationTime}
-              number={true}
-            />
-          </div>
-        </div>
-      </div>
-    </section>
-
-    {#snippet stickyBottom()}
-      {#if editingLink}
-        <Button fullWidth onclick={handleEditLink}>{$t('confirm')}</Button>
-      {:else}
-        <Button fullWidth onclick={handleCreateSharedLink}>{$t('create_link')}</Button>
-      {/if}
-    {/snippet}
-  </FullScreenModal>
-{:else}
-  <QrCodeModal title={$t('view_link')} {onClose} value={sharedLink} />
-{/if}
diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts
index 285bccae30..f13d008a3c 100644
--- a/web/src/lib/constants.ts
+++ b/web/src/lib/constants.ts
@@ -367,8 +367,6 @@ export enum SettingInputFieldType {
 }
 
 export const AlbumPageViewMode = {
-  LINK_SHARING: 'link-sharing',
-  SELECT_USERS: 'select-users',
   SELECT_THUMBNAIL: 'select-thumbnail',
   SELECT_ASSETS: 'select-assets',
   VIEW_USERS: 'view-users',
@@ -377,8 +375,6 @@ export const AlbumPageViewMode = {
 };
 
 export type AlbumPageViewMode =
-  | typeof AlbumPageViewMode.LINK_SHARING
-  | typeof AlbumPageViewMode.SELECT_USERS
   | typeof AlbumPageViewMode.SELECT_THUMBNAIL
   | typeof AlbumPageViewMode.SELECT_ASSETS
   | typeof AlbumPageViewMode.VIEW_USERS
diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/modals/AlbumShareModal.svelte
similarity index 89%
rename from web/src/lib/components/album-page/user-selection-modal.svelte
rename to web/src/lib/modals/AlbumShareModal.svelte
index 9ee7cc550d..56e9a92305 100644
--- a/web/src/lib/components/album-page/user-selection-modal.svelte
+++ b/web/src/lib/modals/AlbumShareModal.svelte
@@ -3,8 +3,8 @@
   import Dropdown from '$lib/components/elements/dropdown.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
-  import QrCodeModal from '$lib/components/shared-components/qr-code-modal.svelte';
   import { AppRoute } from '$lib/constants';
+  import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
   import { serverConfig } from '$lib/stores/server-config.store';
   import { makeSharedLinkUrl } from '$lib/utils';
   import {
@@ -20,16 +20,14 @@
   import { mdiCheck, mdiEye, mdiLink, mdiPencil } from '@mdi/js';
   import { onMount } from 'svelte';
   import { t } from 'svelte-i18n';
-  import UserAvatar from '../shared-components/user-avatar.svelte';
+  import UserAvatar from '../components/shared-components/user-avatar.svelte';
 
   interface Props {
     album: AlbumResponseDto;
-    onClose: () => void;
-    onSelect: (selectedUsers: AlbumUserAddDto[]) => void;
-    onShare: () => void;
+    onClose: (result?: { action: 'sharedLink' } | { action: 'sharedUsers'; data: AlbumUserAddDto[] }) => void;
   }
 
-  let { album, onClose, onSelect, onShare }: Props = $props();
+  let { album, onClose }: Props = $props();
 
   let users: UserResponseDto[] = $state([]);
   let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = $state({});
@@ -160,8 +158,10 @@
           shape="round"
           disabled={Object.keys(selectedUsers).length === 0}
           onclick={() =>
-            onSelect(Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })))}
-          >{$t('add')}</Button
+            onClose({
+              action: 'sharedUsers',
+              data: Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
+            })}>{$t('add')}</Button
         >
       </div>
     {/if}
@@ -182,7 +182,13 @@
         </Stack>
       {/if}
 
-      <Button leadingIcon={mdiLink} size="small" shape="round" fullWidth onclick={onShare}>{$t('create_link')}</Button>
+      <Button
+        leadingIcon={mdiLink}
+        size="small"
+        shape="round"
+        fullWidth
+        onclick={() => onClose({ action: 'sharedLink' })}>{$t('create_link')}</Button
+      >
     </Stack>
   </FullScreenModal>
 {/if}
diff --git a/web/src/lib/components/shared-components/qr-code-modal.svelte b/web/src/lib/modals/QrCodeModal.svelte
similarity index 75%
rename from web/src/lib/components/shared-components/qr-code-modal.svelte
rename to web/src/lib/modals/QrCodeModal.svelte
index 166b2837ee..c56fda801b 100644
--- a/web/src/lib/components/shared-components/qr-code-modal.svelte
+++ b/web/src/lib/modals/QrCodeModal.svelte
@@ -1,24 +1,23 @@
 <script lang="ts">
-  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import QRCode from '$lib/components/shared-components/qrcode.svelte';
   import { copyToClipboard } from '$lib/utils';
-  import { HStack, IconButton, Input } from '@immich/ui';
+  import { HStack, IconButton, Input, Modal, ModalBody } from '@immich/ui';
   import { mdiContentCopy, mdiLink } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
   type Props = {
     title: string;
-    onClose: () => void;
     value: string;
+    onClose: () => void;
   };
 
-  let { onClose, title, value }: Props = $props();
+  let { title, value, onClose }: Props = $props();
 
   let modalWidth = $state(0);
 </script>
 
-<FullScreenModal {title} icon={mdiLink} {onClose}>
-  <div class="w-full">
+<Modal {title} icon={mdiLink} {onClose} size="small">
+  <ModalBody>
     <div class="w-full py-2 px-10">
       <div bind:clientWidth={modalWidth} class="w-full">
         <QRCode {value} width={modalWidth} />
@@ -37,5 +36,5 @@
         />
       </div>
     </HStack>
-  </div>
-</FullScreenModal>
+  </ModalBody>
+</Modal>
diff --git a/web/src/lib/modals/SharedLinkCreateModal.svelte b/web/src/lib/modals/SharedLinkCreateModal.svelte
new file mode 100644
index 0000000000..b4b9eaf98f
--- /dev/null
+++ b/web/src/lib/modals/SharedLinkCreateModal.svelte
@@ -0,0 +1,235 @@
+<script lang="ts">
+  import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
+  import { SettingInputFieldType } from '$lib/constants';
+  import { locale } from '$lib/stores/preferences.store';
+  import { handleError } from '$lib/utils/handle-error';
+  import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
+  import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
+  import { mdiLink } from '@mdi/js';
+  import { DateTime, Duration } from 'luxon';
+  import { t } from 'svelte-i18n';
+  import { NotificationType, notificationController } from '../components/shared-components/notification/notification';
+  import SettingInputField from '../components/shared-components/settings/setting-input-field.svelte';
+  import SettingSwitch from '../components/shared-components/settings/setting-switch.svelte';
+
+  interface Props {
+    onClose: (sharedLink?: SharedLinkResponseDto) => void;
+    albumId?: string | undefined;
+    assetIds?: string[];
+    editingLink?: SharedLinkResponseDto | undefined;
+  }
+
+  let { onClose, albumId = $bindable(undefined), assetIds = $bindable([]), editingLink = undefined }: Props = $props();
+
+  let sharedLink: string | null = $state(null);
+  let description = $state('');
+  let allowDownload = $state(true);
+  let allowUpload = $state(false);
+  let showMetadata = $state(true);
+  let expirationOption: number = $state(0);
+  let password = $state('');
+  let shouldChangeExpirationTime = $state(false);
+  let enablePassword = $state(false);
+
+  const expirationOptions: [number, Intl.RelativeTimeFormatUnit][] = [
+    [30, 'minutes'],
+    [1, 'hour'],
+    [6, 'hours'],
+    [1, 'day'],
+    [7, 'days'],
+    [30, 'days'],
+    [3, 'months'],
+    [1, 'year'],
+  ];
+
+  let relativeTime = $derived(new Intl.RelativeTimeFormat($locale));
+  let expiredDateOptions = $derived([
+    { text: $t('never'), value: 0 },
+    ...expirationOptions.map(([value, unit]) => ({
+      text: relativeTime.format(value, unit),
+      value: Duration.fromObject({ [unit]: value }).toMillis(),
+    })),
+  ]);
+
+  let shareType = $derived(albumId ? SharedLinkType.Album : SharedLinkType.Individual);
+
+  $effect(() => {
+    if (!showMetadata) {
+      allowDownload = false;
+    }
+  });
+
+  if (editingLink) {
+    if (editingLink.description) {
+      description = editingLink.description;
+    }
+    if (editingLink.password) {
+      password = editingLink.password;
+    }
+    allowUpload = editingLink.allowUpload;
+    allowDownload = editingLink.allowDownload;
+    showMetadata = editingLink.showMetadata;
+
+    albumId = editingLink.album?.id;
+    assetIds = editingLink.assets.map(({ id }) => id);
+
+    enablePassword = !!editingLink.password;
+  }
+
+  const handleCreateSharedLink = async () => {
+    const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : undefined;
+
+    try {
+      const data = await createSharedLink({
+        sharedLinkCreateDto: {
+          type: shareType,
+          albumId,
+          assetIds,
+          expiresAt: expirationDate,
+          allowUpload,
+          description,
+          password,
+          allowDownload,
+          showMetadata,
+        },
+      });
+      onClose(data);
+    } catch (error) {
+      handleError(error, $t('errors.failed_to_create_shared_link'));
+    }
+  };
+
+  const handleEditLink = async () => {
+    if (!editingLink) {
+      return;
+    }
+
+    try {
+      const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : null;
+
+      await updateSharedLink({
+        id: editingLink.id,
+        sharedLinkEditDto: {
+          description,
+          password: enablePassword ? password : '',
+          expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
+          allowUpload,
+          allowDownload,
+          showMetadata,
+        },
+      });
+
+      notificationController.show({
+        type: NotificationType.Info,
+        message: $t('edited'),
+      });
+
+      onClose();
+    } catch (error) {
+      handleError(error, $t('errors.failed_to_edit_shared_link'));
+    }
+  };
+
+  const getTitle = () => {
+    if (sharedLink) {
+      return $t('view_link');
+    }
+    if (editingLink) {
+      return $t('edit_link');
+    }
+    return $t('create_link_to_share');
+  };
+</script>
+
+<Modal title={getTitle()} icon={mdiLink} size="small" {onClose}>
+  <ModalBody>
+    {#if shareType === SharedLinkType.Album}
+      {#if !editingLink}
+        <div>{$t('album_with_link_access')}</div>
+      {:else}
+        <div class="text-sm">
+          {$t('public_album')} |
+          <span class="text-immich-primary dark:text-immich-dark-primary">{editingLink.album?.albumName}</span>
+        </div>
+      {/if}
+    {/if}
+
+    {#if shareType === SharedLinkType.Individual}
+      {#if !editingLink}
+        <div>{$t('create_link_to_share_description')}</div>
+      {:else}
+        <div class="text-sm">
+          {$t('individual_share')} |
+          <span class="text-immich-primary dark:text-immich-dark-primary">{editingLink.description || ''}</span>
+        </div>
+      {/if}
+    {/if}
+
+    <div class="mb-2 mt-4">
+      <p class="text-xs">{$t('link_options').toUpperCase()}</p>
+    </div>
+    <div class="rounded-lg bg-gray-100 p-4 dark:bg-black/40 overflow-y-auto">
+      <div class="flex flex-col">
+        <div class="mb-2">
+          <SettingInputField
+            inputType={SettingInputFieldType.TEXT}
+            label={$t('description')}
+            bind:value={description}
+          />
+        </div>
+
+        <div class="mb-2">
+          <SettingInputField
+            inputType={SettingInputFieldType.TEXT}
+            label={$t('password')}
+            bind:value={password}
+            disabled={!enablePassword}
+          />
+        </div>
+
+        <div class="my-3">
+          <SettingSwitch bind:checked={enablePassword} title={$t('require_password')} />
+        </div>
+
+        <div class="my-3">
+          <SettingSwitch bind:checked={showMetadata} title={$t('show_metadata')} />
+        </div>
+
+        <div class="my-3">
+          <SettingSwitch
+            bind:checked={allowDownload}
+            title={$t('allow_public_user_to_download')}
+            disabled={!showMetadata}
+          />
+        </div>
+
+        <div class="my-3">
+          <SettingSwitch bind:checked={allowUpload} title={$t('allow_public_user_to_upload')} />
+        </div>
+
+        {#if editingLink}
+          <div class="my-3">
+            <SettingSwitch bind:checked={shouldChangeExpirationTime} title={$t('change_expiration_time')} />
+          </div>
+        {/if}
+        <div class="mt-3">
+          <SettingSelect
+            bind:value={expirationOption}
+            options={expiredDateOptions}
+            label={$t('expire_after')}
+            disabled={editingLink && !shouldChangeExpirationTime}
+            number={true}
+          />
+        </div>
+      </div>
+    </div>
+  </ModalBody>
+
+  <ModalFooter>
+    {#if editingLink}
+      <Button fullWidth onclick={handleEditLink}>{$t('confirm')}</Button>
+    {:else}
+      <Button fullWidth onclick={handleCreateSharedLink}>{$t('create_link')}</Button>
+    {/if}
+  </ModalFooter>
+</Modal>
diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 7f996396d8..2331ae01b1 100644
--- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -7,7 +7,6 @@
   import AlbumSummary from '$lib/components/album-page/album-summary.svelte';
   import AlbumTitle from '$lib/components/album-page/album-title.svelte';
   import ShareInfoModal from '$lib/components/album-page/share-info-modal.svelte';
-  import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte';
   import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte';
   import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte';
   import Button from '$lib/components/elements/buttons/button.svelte';
@@ -29,7 +28,6 @@
   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
   import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
-  import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
   import {
     NotificationType,
     notificationController,
@@ -37,12 +35,17 @@
   import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
   import { AlbumPageViewMode, AppRoute } from '$lib/constants';
   import { activityManager } from '$lib/managers/activity-manager.svelte';
+  import { modalManager } from '$lib/managers/modal-manager.svelte';
+  import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
+  import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
+  import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import { AssetStore } from '$lib/stores/assets-store.svelte';
+  import { serverConfig } from '$lib/stores/server-config.store';
   import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
   import { preferences, user } from '$lib/stores/user.store';
-  import { handlePromiseError } from '$lib/utils';
+  import { handlePromiseError, makeSharedLinkUrl } from '$lib/utils';
   import { confirmAlbumDelete } from '$lib/utils/album-utils';
   import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
   import { openFileUploadDialog } from '$lib/utils/file-uploader';
@@ -178,10 +181,6 @@
 
   const handleEscape = async () => {
     assetStore.suspendTransitions = true;
-    if (viewMode === AlbumPageViewMode.SELECT_USERS) {
-      viewMode = AlbumPageViewMode.VIEW;
-      return;
-    }
     if (viewMode === AlbumPageViewMode.SELECT_THUMBNAIL) {
       viewMode = AlbumPageViewMode.VIEW;
       return;
@@ -190,10 +189,6 @@
       await handleCloseSelectAssets();
       return;
     }
-    if (viewMode === AlbumPageViewMode.LINK_SHARING) {
-      viewMode = AlbumPageViewMode.VIEW;
-      return;
-    }
     if (viewMode === AlbumPageViewMode.OPTIONS) {
       viewMode = AlbumPageViewMode.VIEW;
       return;
@@ -423,6 +418,31 @@
   const currentAssetIntersection = $derived(
     viewMode === AlbumPageViewMode.SELECT_ASSETS ? timelineInteraction : assetInteraction,
   );
+
+  const handleShare = async () => {
+    const result = await modalManager.show(AlbumShareModal, { album });
+
+    switch (result?.action) {
+      case 'sharedLink': {
+        await handleShareLink();
+        return;
+      }
+
+      case 'sharedUsers': {
+        await handleAddUsers(result.data);
+        return;
+      }
+    }
+  };
+
+  const handleShareLink = async () => {
+    const sharedLink = await modalManager.show(SharedLinkCreateModal, { albumId: album.id });
+
+    if (sharedLink) {
+      const url = makeSharedLinkUrl($serverConfig.externalDomain, sharedLink.key);
+      await modalManager.show(QrCodeModal, { title: $t('view_link'), value: url });
+    }
+  };
 </script>
 
 <div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}>
@@ -496,11 +516,7 @@
             {/if}
 
             {#if isOwned}
-              <CircleIconButton
-                title={$t('share')}
-                onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
-                icon={mdiShareVariantOutline}
-              />
+              <CircleIconButton title={$t('share')} onclick={handleShare} icon={mdiShareVariantOutline} />
             {/if}
 
             <AlbumMap {album} />
@@ -530,12 +546,7 @@
             {/if}
 
             {#if isCreatingSharedAlbum && album.albumUsers.length === 0}
-              <Button
-                size="sm"
-                rounded="lg"
-                disabled={album.assetCount === 0}
-                onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
-              >
+              <Button size="sm" rounded="lg" disabled={album.assetCount === 0} onclick={handleShare}>
                 {$t('share')}
               </Button>
             {/if}
@@ -619,7 +630,7 @@
                       color="gray"
                       size="20"
                       icon={mdiLink}
-                      onclick={() => (viewMode = AlbumPageViewMode.LINK_SHARING)}
+                      onclick={handleShareLink}
                     />
                   {/if}
 
@@ -651,7 +662,7 @@
                       color="gray"
                       size="20"
                       icon={mdiPlus}
-                      onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
+                      onclick={handleShare}
                       title={$t('add_more_users')}
                     />
                   {/if}
@@ -714,18 +725,6 @@
     </div>
   {/if}
 </div>
-{#if viewMode === AlbumPageViewMode.SELECT_USERS}
-  <UserSelectionModal
-    {album}
-    onSelect={handleAddUsers}
-    onShare={() => (viewMode = AlbumPageViewMode.LINK_SHARING)}
-    onClose={() => (viewMode = AlbumPageViewMode.VIEW)}
-  />
-{/if}
-
-{#if viewMode === AlbumPageViewMode.LINK_SHARING}
-  <CreateSharedLinkModal albumId={album.id} onClose={() => (viewMode = AlbumPageViewMode.VIEW)} />
-{/if}
 
 {#if viewMode === AlbumPageViewMode.VIEW_USERS}
   <ShareInfoModal
@@ -749,7 +748,7 @@
     onRefreshAlbum={refreshAlbum}
     onClose={() => (viewMode = AlbumPageViewMode.VIEW)}
     onToggleEnabledActivity={handleToggleEnableActivity}
-    onShowSelectSharedUser={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
+    onShowSelectSharedUser={handleShare}
   />
 {/if}
 
diff --git a/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte b/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte
index 6fd1f17ecc..1c1e1cfbd4 100644
--- a/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte
+++ b/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte
@@ -3,7 +3,6 @@
   import { page } from '$app/state';
   import GroupTab from '$lib/components/elements/group-tab.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
-  import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import {
     notificationController,
@@ -11,6 +10,7 @@
   } from '$lib/components/shared-components/notification/notification';
   import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
   import { AppRoute } from '$lib/constants';
+  import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
   import { handleError } from '$lib/utils/handle-error';
   import { getAllSharedLinks, removeSharedLink, SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
   import { onMount } from 'svelte';
@@ -113,7 +113,7 @@
     {/if}
 
     {#if sharedLink}
-      <CreateSharedLinkModal editingLink={sharedLink} onClose={handleEditDone} />
+      <SharedLinkCreateModal editingLink={sharedLink} onClose={handleEditDone} />
     {/if}
   </div>
 </UserPageLayout>