diff --git a/.github/package.json b/.github/package.json index 1cb0262c74..9b41cc7b4e 100644 --- a/.github/package.json +++ b/.github/package.json @@ -4,6 +4,6 @@ "format:fix": "prettier --write ." }, "devDependencies": { - "prettier": "^3.5.3" + "prettier": "^3.7.4" } } diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 9495d03bb9..b49f63fc68 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -222,6 +222,7 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: '3.3' + bundler-cache: true working-directory: ./mobile/ios - name: Install CocoaPods dependencies @@ -229,13 +230,6 @@ jobs: run: | pod install - - name: Install Fastlane - working-directory: ./mobile/ios - run: | - gem install bundler - bundle config set --local path 'vendor/bundle' - bundle install - - name: Create API Key env: API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} diff --git a/cli/package.json b/cli/package.json index b64354ee4a..e74425eb41 100644 --- a/cli/package.json +++ b/cli/package.json @@ -31,7 +31,7 @@ "eslint-plugin-unicorn": "^62.0.0", "globals": "^16.0.0", "mock-fs": "^5.2.0", - "prettier": "^3.2.5", + "prettier": "^3.7.4", "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", "typescript-eslint": "^8.28.0", diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 6fa1c51bdd..4b1a69d133 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -58,10 +58,6 @@ services: IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://docs.immich.app IMMICH_THIRD_PARTY_SUPPORT_URL: https://docs.immich.app/community-guides - ulimits: - nofile: - soft: 1048576 - hard: 1048576 ports: - 9230:9230 - 9231:9231 @@ -100,10 +96,6 @@ services: - app-node_modules:/usr/src/app/node_modules - sveltekit:/usr/src/app/web/.svelte-kit - coverage:/usr/src/app/web/coverage - ulimits: - nofile: - soft: 1048576 - hard: 1048576 restart: unless-stopped depends_on: immich-server: diff --git a/docs/docs/administration/reverse-proxy.md b/docs/docs/administration/reverse-proxy.md index f766f42719..765c889e23 100644 --- a/docs/docs/administration/reverse-proxy.md +++ b/docs/docs/administration/reverse-proxy.md @@ -32,8 +32,6 @@ server { # enable websockets: http://nginx.org/en/docs/http/websocket.html proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; proxy_redirect off; # set timeout @@ -43,6 +41,8 @@ server { location / { proxy_pass http://:2283; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; } # useful when using Let's Encrypt http-01 challenge diff --git a/docs/package.json b/docs/package.json index b96059c523..d37b256a3f 100644 --- a/docs/package.json +++ b/docs/package.json @@ -38,7 +38,7 @@ "@docusaurus/module-type-aliases": "~3.9.0", "@docusaurus/tsconfig": "^3.7.0", "@docusaurus/types": "^3.7.0", - "prettier": "^3.2.4", + "prettier": "^3.7.4", "typescript": "^5.1.6" }, "browserslist": { diff --git a/e2e/package.json b/e2e/package.json index 7bf61ea232..70669a8546 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -36,14 +36,14 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^62.0.0", - "exiftool-vendored": "^33.0.0", + "exiftool-vendored": "^34.0.0", "globals": "^16.0.0", "jose": "^5.6.3", "luxon": "^3.4.4", "oidc-provider": "^9.0.0", "pg": "^8.11.3", "pngjs": "^7.0.0", - "prettier": "^3.2.5", + "prettier": "^3.7.4", "prettier-plugin-organize-imports": "^4.0.0", "sharp": "^0.34.5", "socket.io-client": "^4.7.4", diff --git a/i18n/en.json b/i18n/en.json index 4b7984bbc6..b206476439 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -78,7 +78,6 @@ "exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.", "export_config_as_json_description": "Download the current system config as a JSON file", "external_libraries_page_description": "Admin external library page", - "external_library_management": "External Library Management", "face_detection": "Face detection", "face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.", "facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.", diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/drift_activities.page.dart index b92d429aa1..ac0cd7f309 100644 --- a/mobile/lib/presentation/pages/drift_activities.page.dart +++ b/mobile/lib/presentation/pages/drift_activities.page.dart @@ -37,7 +37,7 @@ class DriftActivitiesPage extends HookConsumerWidget { child: Scaffold( appBar: AppBar( title: Text(album.name), - actions: [const LikeActivityActionButton(menuItem: true)], + actions: [const LikeActivityActionButton(iconOnly: true)], actionsPadding: const EdgeInsets.only(right: 8), ), body: activities.widgetWhen( diff --git a/mobile/lib/presentation/pages/drift_create_album.page.dart b/mobile/lib/presentation/pages/drift_create_album.page.dart index 57e5cb09a9..f1cbdb13ff 100644 --- a/mobile/lib/presentation/pages/drift_create_album.page.dart +++ b/mobile/lib/presentation/pages/drift_create_album.page.dart @@ -27,8 +27,19 @@ class _DriftCreateAlbumPageState extends ConsumerState { bool isAlbumTitleTextFieldFocus = false; Set selectedAssets = {}; + @override + void initState() { + super.initState(); + albumTitleController.addListener(_onTitleChanged); + } + + void _onTitleChanged() { + setState(() {}); + } + @override void dispose() { + albumTitleController.removeListener(_onTitleChanged); albumTitleController.dispose(); albumDescriptionController.dispose(); albumTitleTextFieldFocusNode.dispose(); diff --git a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart index 71fedf1258..acd7ede6dc 100644 --- a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart @@ -21,12 +21,36 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee enum AddToMenuItem { album, archive, unarchive, lockedFolder } -class AddActionButton extends ConsumerWidget { - const AddActionButton({super.key}); +class AddActionButton extends ConsumerStatefulWidget { + const AddActionButton({super.key, this.originalTheme}); - Future _showAddOptions(BuildContext context, WidgetRef ref) async { + final ThemeData? originalTheme; + + @override + ConsumerState createState() => _AddActionButtonState(); +} + +class _AddActionButtonState extends ConsumerState { + void _handleMenuSelection(AddToMenuItem selected) { + switch (selected) { + case AddToMenuItem.album: + _openAlbumSelector(); + break; + case AddToMenuItem.archive: + performArchiveAction(context, ref, source: ActionSource.viewer); + break; + case AddToMenuItem.unarchive: + performUnArchiveAction(context, ref, source: ActionSource.viewer); + break; + case AddToMenuItem.lockedFolder: + performMoveToLockFolderAction(context, ref, source: ActionSource.viewer); + break; + } + } + + List _buildMenuChildren() { final asset = ref.read(currentAssetNotifier); - if (asset == null) return; + if (asset == null) return []; final user = ref.read(currentUserProvider); final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; @@ -35,93 +59,57 @@ class AddActionButton extends ConsumerWidget { final hasRemote = asset is RemoteAsset; final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived; final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived; - final menuItemHeight = 30.0; - final List> items = [ - PopupMenuItem( - enabled: false, - textStyle: context.textTheme.labelMedium, - height: 40, - child: Text("add_to_bottom_bar".tr()), + return [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text("add_to_bottom_bar".tr(), style: context.textTheme.labelMedium), ), - PopupMenuItem( - height: menuItemHeight, - value: AddToMenuItem.album, - child: ListTile(leading: const Icon(Icons.photo_album_outlined), title: Text("album".tr())), + BaseActionButton( + iconData: Icons.photo_album_outlined, + label: "album".tr(), + menuItem: true, + onPressed: () => _handleMenuSelection(AddToMenuItem.album), ), - const PopupMenuDivider(), - PopupMenuItem(enabled: false, textStyle: context.textTheme.labelMedium, height: 40, child: Text("move_to".tr())), + if (isOwner) ...[ + const Divider(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text("move_to".tr(), style: context.textTheme.labelMedium), + ), if (showArchive) - PopupMenuItem( - height: menuItemHeight, - value: AddToMenuItem.archive, - child: ListTile(leading: const Icon(Icons.archive_outlined), title: Text("archive".tr())), + BaseActionButton( + iconData: Icons.archive_outlined, + label: "archive".tr(), + menuItem: true, + onPressed: () => _handleMenuSelection(AddToMenuItem.archive), ), if (showUnarchive) - PopupMenuItem( - height: menuItemHeight, - value: AddToMenuItem.unarchive, - child: ListTile(leading: const Icon(Icons.unarchive_outlined), title: Text("unarchive".tr())), + BaseActionButton( + iconData: Icons.unarchive_outlined, + label: "unarchive".tr(), + menuItem: true, + onPressed: () => _handleMenuSelection(AddToMenuItem.unarchive), ), - PopupMenuItem( - height: menuItemHeight, - value: AddToMenuItem.lockedFolder, - child: ListTile(leading: const Icon(Icons.lock_outline), title: Text("locked_folder".tr())), + BaseActionButton( + iconData: Icons.lock_outline, + label: "locked_folder".tr(), + menuItem: true, + onPressed: () => _handleMenuSelection(AddToMenuItem.lockedFolder), ), ], ]; - - final AddToMenuItem? selected = await showMenu( - context: context, - color: context.themeData.scaffoldBackgroundColor, - position: _menuPosition(context), - items: items, - popUpAnimationStyle: AnimationStyle.noAnimation, - ); - - if (selected == null) { - return; - } - - switch (selected) { - case AddToMenuItem.album: - _openAlbumSelector(context, ref); - break; - case AddToMenuItem.archive: - await performArchiveAction(context, ref, source: ActionSource.viewer); - break; - case AddToMenuItem.unarchive: - await performUnArchiveAction(context, ref, source: ActionSource.viewer); - break; - case AddToMenuItem.lockedFolder: - await performMoveToLockFolderAction(context, ref, source: ActionSource.viewer); - break; - } } - RelativeRect _menuPosition(BuildContext context) { - final renderObject = context.findRenderObject(); - if (renderObject is! RenderBox) { - return RelativeRect.fill; - } - - final size = renderObject.size; - final position = renderObject.localToGlobal(Offset.zero); - - return RelativeRect.fromLTRB(position.dx, position.dy - size.height - 200, position.dx + size.width, position.dy); - } - - void _openAlbumSelector(BuildContext context, WidgetRef ref) { + void _openAlbumSelector() { final currentAsset = ref.read(currentAssetNotifier); if (currentAsset == null) { ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error); return; } - final List slivers = [ - AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(context, ref, album)), - ]; + final List slivers = [AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album))]; showModalBottomSheet( context: context, @@ -141,7 +129,7 @@ class AddActionButton extends ConsumerWidget { ); } - Future _addCurrentAssetToAlbum(BuildContext context, WidgetRef ref, RemoteAlbum album) async { + Future _addCurrentAssetToAlbum(RemoteAlbum album) async { final latest = ref.read(currentAssetNotifier); if (latest == null) { @@ -174,17 +162,36 @@ class AddActionButton extends ConsumerWidget { } @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final asset = ref.watch(currentAssetNotifier); if (asset == null) { return const SizedBox.shrink(); } - return Builder( - builder: (buttonContext) { + + final themeData = widget.originalTheme ?? context.themeData; + + return MenuAnchor( + consumeOutsideTap: true, + style: MenuStyle( + backgroundColor: WidgetStatePropertyAll(themeData.scaffoldBackgroundColor), + elevation: const WidgetStatePropertyAll(4), + shape: const WidgetStatePropertyAll( + RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), + ), + ), + menuChildren: widget.originalTheme != null + ? [ + Theme( + data: widget.originalTheme!, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: _buildMenuChildren()), + ), + ] + : _buildMenuChildren(), + builder: (context, controller, child) { return BaseActionButton( iconData: Icons.add, label: "add_to_bottom_bar".tr(), - onPressed: () => _showAddOptions(buttonContext, ref), + onPressed: () => controller.isOpen ? controller.close() : controller.open(), ); }, ); diff --git a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart index b62398c03e..e6098b07b4 100644 --- a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart @@ -11,6 +11,7 @@ class BaseActionButton extends StatelessWidget { this.onLongPressed, this.maxWidth = 90.0, this.minWidth, + this.iconOnly = false, this.menuItem = false, }); @@ -19,6 +20,11 @@ class BaseActionButton extends StatelessWidget { final Color? iconColor; final double maxWidth; final double? minWidth; + + /// When true, renders only an IconButton without text label + final bool iconOnly; + + /// When true, renders as a MenuItemButton for use in MenuAnchor menus final bool menuItem; final void Function()? onPressed; final void Function()? onLongPressed; @@ -31,33 +37,23 @@ class BaseActionButton extends StatelessWidget { final iconColor = this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color; final textColor = context.themeData.textTheme.labelLarge?.color; - if (menuItem) { + if (iconOnly) { return IconButton( onPressed: onPressed, icon: Icon(iconData, size: iconSize, color: iconColor), ); } - if (context.findAncestorWidgetOfExactType() != null) { + if (menuItem) { final theme = context.themeData; - final textStyle = theme.textTheme.bodyMedium; - final defaultTextColor = theme.colorScheme.onSurfaceVariant; - final effectiveStyle = (textStyle ?? theme.textTheme.bodyMedium)?.copyWith( - color: (textStyle?.color ?? defaultTextColor), - ); + final effectiveStyle = theme.textTheme.labelLarge; final effectiveIconColor = iconColor ?? theme.iconTheme.color ?? theme.colorScheme.onSurfaceVariant; return MenuItemButton( - style: MenuItemButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - visualDensity: const VisualDensity(vertical: -2.5), - ), - trailingIcon: Icon(iconData, size: 18, color: effectiveIconColor), + style: MenuItemButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12)), + leadingIcon: Icon(iconData, color: effectiveIconColor, size: 20), onPressed: onPressed, - child: Align( - alignment: Alignment.centerLeft, - child: Text(label, style: effectiveStyle), - ), + child: Text(label, style: effectiveStyle), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart index 26b8ba6f47..2840ad294b 100644 --- a/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart @@ -7,8 +7,9 @@ import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; class CastActionButton extends ConsumerWidget { - const CastActionButton({super.key, this.menuItem = true}); + const CastActionButton({super.key, this.iconOnly = true, this.menuItem = false}); + final bool iconOnly; final bool menuItem; @override @@ -22,6 +23,7 @@ class CastActionButton extends ConsumerWidget { onPressed: () { showDialog(context: context, builder: (context) => const CastDialog()); }, + iconOnly: iconOnly, menuItem: menuItem, ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart index cb898f069a..a5129b643a 100644 --- a/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart @@ -10,8 +10,9 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; class DownloadActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; final bool menuItem; - const DownloadActionButton({super.key, required this.source, this.menuItem = false}); + const DownloadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async { if (!context.mounted) { @@ -38,6 +39,7 @@ class DownloadActionButton extends ConsumerWidget { iconData: Icons.download, maxWidth: 95, label: "download".t(context: context), + iconOnly: iconOnly, menuItem: menuItem, onPressed: () => _onTap(context, ref, backgroundManager), ); diff --git a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart index 0aca5158ef..ba2491365d 100644 --- a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart @@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; class FavoriteActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; final bool menuItem; - const FavoriteActionButton({super.key, required this.source, this.menuItem = false}); + const FavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -44,6 +45,7 @@ class FavoriteActionButton extends ConsumerWidget { return BaseActionButton( iconData: Icons.favorite_border_rounded, label: "favorite".t(context: context), + iconOnly: iconOnly, menuItem: menuItem, onPressed: () => _onTap(context, ref), ); diff --git a/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart index 33794eae11..a61f72ea01 100644 --- a/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart @@ -12,8 +12,9 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da import 'package:immich_mobile/providers/user.provider.dart'; class LikeActivityActionButton extends ConsumerWidget { - const LikeActivityActionButton({super.key, this.menuItem = false}); + const LikeActivityActionButton({super.key, this.iconOnly = false, this.menuItem = false}); + final bool iconOnly; final bool menuItem; @override @@ -49,6 +50,7 @@ class LikeActivityActionButton extends ConsumerWidget { iconData: liked != null ? Icons.favorite : Icons.favorite_border, label: "like".t(context: context), onPressed: () => onTap(liked), + iconOnly: iconOnly, menuItem: menuItem, ); }, @@ -57,6 +59,7 @@ class LikeActivityActionButton extends ConsumerWidget { loading: () => BaseActionButton( iconData: Icons.favorite_border, label: "like".t(context: context), + iconOnly: iconOnly, menuItem: menuItem, ), error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])), diff --git a/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart index 696b9ff367..9cf541f49f 100644 --- a/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart @@ -5,8 +5,9 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; class MotionPhotoActionButton extends ConsumerWidget { - const MotionPhotoActionButton({super.key, this.menuItem = true}); + const MotionPhotoActionButton({super.key, this.iconOnly = true, this.menuItem = false}); + final bool iconOnly; final bool menuItem; @override @@ -17,6 +18,7 @@ class MotionPhotoActionButton extends ConsumerWidget { iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded, label: "play_motion_photo".t(context: context), onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle, + iconOnly: iconOnly, menuItem: menuItem, ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart index 7fdc5e81e8..ec5513e0a8 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart @@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; class UnFavoriteActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; final bool menuItem; - const UnFavoriteActionButton({super.key, required this.source, this.menuItem = false}); + const UnFavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -45,6 +46,7 @@ class UnFavoriteActionButton extends ConsumerWidget { iconData: Icons.favorite_rounded, label: "unfavorite".t(context: context), onPressed: () => _onTap(context, ref), + iconOnly: iconOnly, menuItem: menuItem, ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart index 96b42da6c8..56008abc57 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart @@ -82,8 +82,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { } final actions = [ - if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, menuItem: true), - if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true), + if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, iconOnly: true), + if (isCasting || (asset.hasRemote)) const CastActionButton(iconOnly: true), if (album != null && album.isActivityEnabled && album.isShared) IconButton( icon: const Icon(Icons.chat_outlined), @@ -102,16 +102,16 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { tooltip: 'view_in_timeline'.t(context: context), ), if (asset.hasRemote && isOwner && !asset.isFavorite) - const FavoriteActionButton(source: ActionSource.viewer, menuItem: true), + const FavoriteActionButton(source: ActionSource.viewer, iconOnly: true), if (asset.hasRemote && isOwner && asset.isFavorite) - const UnFavoriteActionButton(source: ActionSource.viewer, menuItem: true), - if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true), - ViewerKebabMenu(onConfigureButtons: openConfigurator), + const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true), + if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true), + const ViewerKebabMenu(), ]; final lockedViewActions = [ - if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true), - ViewerKebabMenu(onConfigureButtons: openConfigurator), + if (isCasting || (asset.hasRemote)) const CastActionButton(iconOnly: true), + const ViewerKebabMenu(), ]; return IgnorePointer( diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 30f43cf3b2..383f599331 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -167,7 +167,7 @@ class AppRouter extends RootStackRouter { AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]), AutoRoute(page: ChangePasswordRoute.page), AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), - CustomRoute( + AutoRoute( page: TabControllerRoute.page, guards: [_authGuard, _duplicateGuard], children: [ @@ -176,9 +176,8 @@ class AppRouter extends RootStackRouter { AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), ], - transitionsBuilder: TransitionsBuilders.fadeIn, ), - CustomRoute( + AutoRoute( page: TabShellRoute.page, guards: [_authGuard, _duplicateGuard], children: [ @@ -187,7 +186,6 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftLibraryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), ], - transitionsBuilder: TransitionsBuilders.fadeIn, ), CustomRoute( page: GalleryViewerRoute.page, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43d2848e16..09a047b9e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,8 +20,8 @@ importers: .github: devDependencies: prettier: - specifier: ^3.5.3 - version: 3.7.1 + specifier: ^3.7.4 + version: 3.7.4 cli: dependencies: @@ -85,7 +85,7 @@ importers: version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.1) + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.4) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.1(jiti@2.6.1)) @@ -96,11 +96,11 @@ importers: specifier: ^5.2.0 version: 5.5.0 prettier: - specifier: ^3.2.5 - version: 3.7.1 + specifier: ^3.7.4 + version: 3.7.4 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.7.1)(typescript@5.9.3) + version: 4.3.0(prettier@3.7.4)(typescript@5.9.3) typescript: specifier: ^5.3.3 version: 5.9.3 @@ -184,8 +184,8 @@ importers: specifier: ^3.7.0 version: 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) prettier: - specifier: ^3.2.4 - version: 3.7.1 + specifier: ^3.7.4 + version: 3.7.4 typescript: specifier: ^5.1.6 version: 5.9.3 @@ -239,13 +239,13 @@ importers: version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.1) + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.4) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.1(jiti@2.6.1)) exiftool-vendored: - specifier: ^33.0.0 - version: 33.5.0 + specifier: ^34.0.0 + version: 34.0.0 globals: specifier: ^16.0.0 version: 16.5.0 @@ -265,11 +265,11 @@ importers: specifier: ^7.0.0 version: 7.0.0 prettier: - specifier: ^3.2.5 - version: 3.7.1 + specifier: ^3.7.4 + version: 3.7.4 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.7.1)(typescript@5.9.3) + version: 4.3.0(prettier@3.7.4)(typescript@5.9.3) sharp: specifier: ^0.34.5 version: 0.34.5 @@ -428,8 +428,8 @@ importers: specifier: 4.3.3 version: 4.3.3 exiftool-vendored: - specifier: ^33.0.0 - version: 33.5.0 + specifier: ^34.0.0 + version: 34.0.0 express: specifier: ^5.1.0 version: 5.2.0 @@ -655,7 +655,7 @@ importers: version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.1) + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.4) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.1(jiti@2.6.1)) @@ -672,11 +672,11 @@ importers: specifier: ^7.0.0 version: 7.0.0 prettier: - specifier: ^3.0.2 - version: 3.7.1 + specifier: ^3.7.4 + version: 3.7.4 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.7.1)(typescript@5.9.3) + version: 4.3.0(prettier@3.7.4)(typescript@5.9.3) sql-formatter: specifier: ^15.0.0 version: 15.6.10 @@ -717,8 +717,8 @@ importers: specifier: file:../open-api/typescript-sdk version: link:../open-api/typescript-sdk '@immich/ui': - specifier: ^0.49.2 - version: 0.49.3(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2) + specifier: ^0.50.0 + version: 0.50.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -904,17 +904,17 @@ importers: specifier: ^16.0.0 version: 16.5.0 prettier: - specifier: ^3.4.2 - version: 3.7.1 + specifier: ^3.7.4 + version: 3.7.4 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.7.1)(typescript@5.9.3) + version: 4.3.0(prettier@3.7.4)(typescript@5.9.3) prettier-plugin-sort-json: specifier: ^4.1.1 - version: 4.1.1(prettier@3.7.1) + version: 4.1.1(prettier@3.7.4) prettier-plugin-svelte: specifier: ^3.3.3 - version: 3.4.0(prettier@3.7.1)(svelte@5.45.2) + version: 3.4.0(prettier@3.7.4)(svelte@5.45.2) rollup-plugin-visualizer: specifier: ^6.0.0 version: 6.0.5(rollup@4.53.3) @@ -2989,8 +2989,8 @@ packages: peerDependencies: svelte: ^5.0.0 - '@immich/ui@0.49.3': - resolution: {integrity: sha512-joqT72Y6gmGK6z25Suzr2VhYANrLo43g20T4UHmbQenz/z/Ax6sl1Ao9SjIOwEkKMm9N3Txoh7WOOzmHVl04OA==} + '@immich/ui@0.50.0': + resolution: {integrity: sha512-7AW9SRZTAgal8xlkUAxm7o4+pSG7HcKb+Bh9JpWLaDRRdGyPCZMmsNa9CjZglOQ7wkAD07tQ9u4+zezBLe0dlQ==} peerDependencies: svelte: ^5.0.0 @@ -3236,6 +3236,7 @@ packages: '@koa/router@14.0.0': resolution: {integrity: sha512-LBSu5K0qAaaQcXX/0WIB9PGDevyCxxpnc1uq13vV/CgObaVxuis5hKl3Eboq/8gcb6ebnkAStW9NB/Em2eYyFA==} engines: {node: '>= 20'} + deprecated: Please upgrade to v15 or higher. All reported bugs in this version are fixed in newer releases, dependencies have been updated, and security has been improved. '@koddsson/eslint-plugin-tscompat@0.2.0': resolution: {integrity: sha512-Oqd4kWSX0LiO9wWHjcmDfXZNC7TotFV/tLRhwCFU3XUeb//KYvJ75c9OmeSJ+vBv5lkCeB+xYsqyNrBc5j18XA==} @@ -5503,8 +5504,8 @@ packages: resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==} hasBin: true - batch-cluster@15.0.1: - resolution: {integrity: sha512-eUmh0ld1AUPKTEmdzwGF9QTSexXAyt9rA1F5zDfW1wUi3okA3Tal4NLdCeFI6aiKpBenQhR6NmK9bW9tBHTGPQ==} + batch-cluster@16.0.0: + resolution: {integrity: sha512-+T7Ho09ikx/kP4P8M+GEnpuePzRQa4gTUhtPIu6ApFC8+0GY0sri1y1PuB+yfXlQWl5DkHC/e58z3U6g0qCz/A==} engines: {node: '>=20'} batch@0.6.1: @@ -6848,17 +6849,17 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} - exiftool-vendored.exe@13.42.0: - resolution: {integrity: sha512-6AFybe5IakduMWleuQBfep9OWGSVZSedt2uKL+LzufRsATp+beOF7tZyKtMztjb6VRH1GF/4F9EvBVam6zm70w==} + exiftool-vendored.exe@13.43.0: + resolution: {integrity: sha512-EENHNz86tYY5yHGPtGB2mto3FIGstQvEhrcU34f7fm4RMxBKNfTWYOGkhU1jzvjOi+V4575LQX/FUES1TwgUbQ==} os: [win32] - exiftool-vendored.pl@13.42.0: - resolution: {integrity: sha512-EF5IdxQNIJIvZjHf4bG4jnwAHVVSLkYZToo2q+Mm89kSuppKfRvHz/lngIxN0JALE8rFdC4zt6NWY/PKqRdCcg==} + exiftool-vendored.pl@13.43.0: + resolution: {integrity: sha512-0ApWaQ/pxaliPK7HzTxVA0sg/wZ8vl7UtFVhCyWhGQg01WfZkFrKwKmELB0Bnn01WTfgIuMadba8ccmFvpmJag==} os: ['!win32'] hasBin: true - exiftool-vendored@33.5.0: - resolution: {integrity: sha512-7cCh6izwdmC5ZaCxpHFehnExIr2Yp7CJuxHg4WFiGcm81yyxXLtvSE+85ep9VsNwhlOtSpk+XxiqrlddjY5lAw==} + exiftool-vendored@34.0.0: + resolution: {integrity: sha512-rhIe4XGE7kh76nwytwHtq6qK/pc1mpOBHRV++gudFeG2PfAp3XIVQbFWCLK3S4l9I4AWYOe4mxk8mW8l1oHRTw==} engines: {node: '>=20.0.0'} expect-type@1.2.1: @@ -9765,8 +9766,8 @@ packages: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier@3.7.1: - resolution: {integrity: sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ==} + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} engines: {node: '>=14'} hasBin: true @@ -14517,7 +14518,7 @@ snapshots: '@fig/complete-commander@3.2.0(commander@11.1.0)': dependencies: commander: 11.1.0 - prettier: 3.7.1 + prettier: 3.7.4 '@floating-ui/core@1.7.3': dependencies: @@ -14700,7 +14701,7 @@ snapshots: dependencies: svelte: 5.45.2 - '@immich/ui@0.49.3(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)': + '@immich/ui@0.50.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)': dependencies: '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.45.2) '@internationalized/date': 3.10.0 @@ -15788,7 +15789,7 @@ snapshots: '@react-email/render@1.4.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: html-to-text: 9.0.5 - prettier: 3.7.1 + prettier: 3.7.4 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) react-promise-suspense: 0.3.4 @@ -17580,7 +17581,7 @@ snapshots: baseline-browser-mapping@2.8.31: {} - batch-cluster@15.0.1: {} + batch-cluster@16.0.0: {} batch@0.6.1: {} @@ -18907,10 +18908,10 @@ snapshots: lodash.memoize: 4.1.2 semver: 7.7.3 - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.1): + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.4): dependencies: eslint: 9.39.1(jiti@2.6.1) - prettier: 3.7.1 + prettier: 3.7.4 prettier-linter-helpers: 1.0.0 synckit: 0.11.11 optionalDependencies: @@ -19128,21 +19129,21 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - exiftool-vendored.exe@13.42.0: + exiftool-vendored.exe@13.43.0: optional: true - exiftool-vendored.pl@13.42.0: {} + exiftool-vendored.pl@13.43.0: {} - exiftool-vendored@33.5.0: + exiftool-vendored@34.0.0: dependencies: '@photostructure/tz-lookup': 11.3.0 '@types/luxon': 3.7.1 - batch-cluster: 15.0.1 - exiftool-vendored.pl: 13.42.0 + batch-cluster: 16.0.0 + exiftool-vendored.pl: 13.43.0 he: 1.2.0 luxon: 3.7.2 optionalDependencies: - exiftool-vendored.exe: 13.42.0 + exiftool-vendored.exe: 13.43.0 expect-type@1.2.1: {} @@ -22636,21 +22637,21 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-organize-imports@4.3.0(prettier@3.7.1)(typescript@5.9.3): + prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3): dependencies: - prettier: 3.7.1 + prettier: 3.7.4 typescript: 5.9.3 - prettier-plugin-sort-json@4.1.1(prettier@3.7.1): + prettier-plugin-sort-json@4.1.1(prettier@3.7.4): dependencies: - prettier: 3.7.1 + prettier: 3.7.4 - prettier-plugin-svelte@3.4.0(prettier@3.7.1)(svelte@5.45.2): + prettier-plugin-svelte@3.4.0(prettier@3.7.4)(svelte@5.45.2): dependencies: - prettier: 3.7.1 + prettier: 3.7.4 svelte: 5.45.2 - prettier@3.7.1: {} + prettier@3.7.4: {} pretty-error@4.0.0: dependencies: diff --git a/server/Dockerfile b/server/Dockerfile index 267253ccd9..918658e19f 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -50,13 +50,15 @@ RUN --mount=type=cache,id=pnpm-cli,target=/buildcache/pnpm-store \ FROM builder AS plugins +ARG TARGETPLATFORM + COPY --from=ghcr.io/jdx/mise:2025.11.3@sha256:ac26f5978c0e2783f3e68e58ce75eddb83e41b89bf8747c503bac2aa9baf22c5 /usr/local/bin/mise /usr/local/bin/mise WORKDIR /usr/src/app COPY ./plugins/mise.toml ./plugins/ ENV MISE_TRUSTED_CONFIG_PATHS=/usr/src/app/plugins/mise.toml ENV MISE_DATA_DIR=/buildcache/mise -RUN --mount=type=cache,id=mise-tools,target=/buildcache/mise \ +RUN --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \ mise install --cd plugins COPY ./plugins ./plugins/ @@ -66,7 +68,7 @@ RUN --mount=type=cache,id=pnpm-plugins,target=/buildcache/pnpm-store \ --mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \ --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ --mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \ - --mount=type=cache,id=mise-tools,target=/buildcache/mise \ + --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \ cd plugins && mise run build FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29 diff --git a/server/package.json b/server/package.json index 915e45c116..d4efb4b1bb 100644 --- a/server/package.json +++ b/server/package.json @@ -70,7 +70,7 @@ "cookie": "^1.0.2", "cookie-parser": "^1.4.7", "cron": "4.3.3", - "exiftool-vendored": "^33.0.0", + "exiftool-vendored": "^34.0.0", "express": "^5.1.0", "fast-glob": "^3.3.2", "fluent-ffmpeg": "^2.1.2", @@ -153,7 +153,7 @@ "mock-fs": "^5.2.0", "node-gyp": "^12.0.0", "pngjs": "^7.0.0", - "prettier": "^3.0.2", + "prettier": "^3.7.4", "prettier-plugin-organize-imports": "^4.0.0", "sql-formatter": "^15.0.0", "supertest": "^7.1.0", diff --git a/server/src/sql-tools/decorators/column.decorator.ts b/server/src/sql-tools/decorators/column.decorator.ts index adb3d0ed59..e5a0eb52f8 100644 --- a/server/src/sql-tools/decorators/column.decorator.ts +++ b/server/src/sql-tools/decorators/column.decorator.ts @@ -2,7 +2,7 @@ import { asOptions } from 'src/sql-tools/helpers'; import { register } from 'src/sql-tools/register'; import { ColumnStorage, ColumnType, DatabaseEnum } from 'src/sql-tools/types'; -export type ColumnValue = null | boolean | string | number | object | Date | (() => string); +export type ColumnValue = null | boolean | string | number | Array | object | Date | (() => string); export type ColumnBaseOptions = { name?: string; diff --git a/server/src/sql-tools/helpers.ts b/server/src/sql-tools/helpers.ts index 2ef35ce9ba..e0daf8262f 100644 --- a/server/src/sql-tools/helpers.ts +++ b/server/src/sql-tools/helpers.ts @@ -39,6 +39,10 @@ export const fromColumnValue = (columnValue?: ColumnValue) => { return `'${value.toISOString()}'`; } + if (Array.isArray(value)) { + return "'{}'"; + } + return `'${String(value)}'`; }; diff --git a/server/src/sql-tools/schema-diff.spec.ts b/server/src/sql-tools/schema-diff.spec.ts index fe249b4e29..f45fb98bd3 100644 --- a/server/src/sql-tools/schema-diff.spec.ts +++ b/server/src/sql-tools/schema-diff.spec.ts @@ -394,6 +394,20 @@ describe(schemaDiff.name, () => { expect(diff.items).toEqual([]); }); + + it('should support arrays, ignoring types', () => { + const diff = schemaDiff( + fromColumn({ name: 'column1', type: 'character varying', isArray: true, default: "'{}'" }), + fromColumn({ + name: 'column1', + type: 'character varying', + isArray: true, + default: "'{}'::character varying[]", + }), + ); + + expect(diff.items).toEqual([]); + }); }); }); diff --git a/server/test/sql-tools/column-default-array.stub.ts b/server/test/sql-tools/column-default-array.stub.ts new file mode 100644 index 0000000000..b5e9b7d04a --- /dev/null +++ b/server/test/sql-tools/column-default-array.stub.ts @@ -0,0 +1,40 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column({ type: 'character varying', array: true, default: [] }) + column1!: string[]; +} + +export const description = 'should register a table with a column with a default value (array)'; +export const schema: DatabaseSchema = { + databaseName: 'postgres', + schemaName: 'public', + functions: [], + enums: [], + extensions: [], + parameters: [], + overrides: [], + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: false, + isArray: true, + primary: false, + synchronize: true, + default: "'{}'", + }, + ], + indexes: [], + triggers: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/web/package.json b/web/package.json index 2e7b740153..82065d74bf 100644 --- a/web/package.json +++ b/web/package.json @@ -28,7 +28,7 @@ "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/justified-layout-wasm": "^0.4.3", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.49.2", + "@immich/ui": "^0.50.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.14.0", @@ -93,7 +93,7 @@ "factory.ts": "^1.4.1", "globals": "^16.0.0", "happy-dom": "^20.0.0", - "prettier": "^3.4.2", + "prettier": "^3.7.4", "prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-svelte": "^3.3.3", diff --git a/web/src/lib/components/HeaderActionButton.svelte b/web/src/lib/components/HeaderActionButton.svelte new file mode 100644 index 0000000000..542c22ba43 --- /dev/null +++ b/web/src/lib/components/HeaderActionButton.svelte @@ -0,0 +1,24 @@ + + +{#if action.$if?.() ?? true} + +{/if} diff --git a/web/src/lib/components/HeaderButton.svelte b/web/src/lib/components/HeaderButton.svelte deleted file mode 100644 index c4189c06c0..0000000000 --- a/web/src/lib/components/HeaderButton.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -{#if action.$if?.() ?? true} - -{/if} diff --git a/web/src/lib/components/album-page/albums-table-row.svelte b/web/src/lib/components/album-page/albums-table-row.svelte index 865eac8366..852bbf166c 100644 --- a/web/src/lib/components/album-page/albums-table-row.svelte +++ b/web/src/lib/components/album-page/albums-table-row.svelte @@ -32,7 +32,7 @@ goto(resolve(`${AppRoute.ALBUMS}/${album.id}`))} {oncontextmenu} > diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index 38d734fc22..63e6f7cc04 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -126,6 +126,7 @@ const onMouseLeave = () => { mouseOver = false; + onMouseEvent?.({ isMouseOver: false, selectedGroupIndex: groupIndex }); }; let timer: ReturnType | null = null; diff --git a/web/src/lib/components/layouts/AdminPageLayout.svelte b/web/src/lib/components/layouts/AdminPageLayout.svelte index 45d21c9139..d63e306853 100644 --- a/web/src/lib/components/layouts/AdminPageLayout.svelte +++ b/web/src/lib/components/layouts/AdminPageLayout.svelte @@ -1,19 +1,33 @@ @@ -24,11 +38,37 @@ - +
+
+ + + {#if actions.length > 0} + + + + {/if} +
{@render children?.()} - +
diff --git a/web/src/lib/components/layouts/TitleLayout.svelte b/web/src/lib/components/layouts/TitleLayout.svelte deleted file mode 100644 index 2d867bab2f..0000000000 --- a/web/src/lib/components/layouts/TitleLayout.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - -
-
- - {@render buttons?.()} -
- {@render children?.()} -
diff --git a/web/src/lib/services/library.service.ts b/web/src/lib/services/library.service.ts index 8b4d35a5f6..d20eae6af6 100644 --- a/web/src/lib/services/library.service.ts +++ b/web/src/lib/services/library.service.ts @@ -28,7 +28,7 @@ export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResp title: $t('scan_all_libraries'), type: $t('command'), icon: mdiSync, - onAction: () => void handleScanAllLibraries(), + onAction: () => handleScanAllLibraries(), shortcuts: { shift: true, key: 'r' }, $if: () => libraries.length > 0, }; @@ -37,7 +37,7 @@ export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResp title: $t('create_library'), type: $t('command'), icon: mdiPlusBoxOutline, - onAction: () => void handleCreateLibrary(), + onAction: () => handleCreateLibrary(), shortcuts: { shift: true, key: 'n' }, }; @@ -49,7 +49,7 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse icon: mdiPencilOutline, type: $t('command'), title: $t('rename'), - onAction: () => void modalManager.show(LibraryRenameModal, { library }), + onAction: () => modalManager.show(LibraryRenameModal, { library }), shortcuts: { key: 'r' }, }; @@ -58,7 +58,7 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse type: $t('command'), title: $t('delete'), color: 'danger', - onAction: () => void handleDeleteLibrary(library), + onAction: () => handleDeleteLibrary(library), shortcuts: { key: 'Backspace' }, }; @@ -66,21 +66,21 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse icon: mdiPlusBoxOutline, type: $t('command'), title: $t('add'), - onAction: () => void modalManager.show(LibraryFolderAddModal, { library }), + onAction: () => modalManager.show(LibraryFolderAddModal, { library }), }; const AddExclusionPattern: ActionItem = { icon: mdiPlusBoxOutline, type: $t('command'), title: $t('add'), - onAction: () => void modalManager.show(LibraryExclusionPatternAddModal, { library }), + onAction: () => modalManager.show(LibraryExclusionPatternAddModal, { library }), }; const Scan: ActionItem = { icon: mdiSync, type: $t('command'), title: $t('scan_library'), - onAction: () => void handleScanLibrary(library), + onAction: () => handleScanLibrary(library), shortcuts: { shift: true, key: 'r' }, }; @@ -92,14 +92,14 @@ export const getLibraryFolderActions = ($t: MessageFormatter, library: LibraryRe icon: mdiPencilOutline, type: $t('command'), title: $t('edit'), - onAction: () => void modalManager.show(LibraryFolderEditModal, { folder, library }), + onAction: () => modalManager.show(LibraryFolderEditModal, { folder, library }), }; const Delete: ActionItem = { icon: mdiTrashCanOutline, type: $t('command'), title: $t('delete'), - onAction: () => void handleDeleteLibraryFolder(library, folder), + onAction: () => handleDeleteLibraryFolder(library, folder), }; return { Edit, Delete }; @@ -114,14 +114,14 @@ export const getLibraryExclusionPatternActions = ( icon: mdiPencilOutline, type: $t('command'), title: $t('edit'), - onAction: () => void modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }), + onAction: () => modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }), }; const Delete: ActionItem = { icon: mdiTrashCanOutline, type: $t('command'), title: $t('delete'), - onAction: () => void handleDeleteExclusionPattern(library, exclusionPattern), + onAction: () => handleDeleteExclusionPattern(library, exclusionPattern), }; return { Edit, Delete }; @@ -273,7 +273,7 @@ const handleDeleteLibraryFolder = async (library: LibraryResponseDto, folder: st }); if (!confirmed) { - return false; + return; } try { @@ -285,10 +285,7 @@ const handleDeleteLibraryFolder = async (library: LibraryResponseDto, folder: st toastManager.success($t('admin.library_updated')); } catch (error) { handleError(error, $t('errors.unable_to_update_library')); - return false; } - - return true; }; export const handleAddLibraryExclusionPattern = async (library: LibraryResponseDto, exclusionPattern: string) => { @@ -345,9 +342,8 @@ const handleDeleteExclusionPattern = async (library: LibraryResponseDto, exclusi const $t = await getFormatter(); const confirmed = await modalManager.showDialog({ prompt: $t('admin.library_remove_exclusion_pattern_prompt') }); - if (!confirmed) { - return false; + return; } try { @@ -361,8 +357,5 @@ const handleDeleteExclusionPattern = async (library: LibraryResponseDto, exclusi toastManager.success($t('admin.library_updated')); } catch (error) { handleError(error, $t('errors.unable_to_update_library')); - return false; } - - return true; }; diff --git a/web/src/lib/services/queue.service.ts b/web/src/lib/services/queue.service.ts index 2372461d1a..46219ef22a 100644 --- a/web/src/lib/services/queue.service.ts +++ b/web/src/lib/services/queue.service.ts @@ -1,11 +1,20 @@ import { goto } from '$app/navigation'; import { AppRoute } from '$lib/constants'; import { eventManager } from '$lib/managers/event-manager.svelte'; +import { queueManager } from '$lib/managers/queue-manager.svelte'; import JobCreateModal from '$lib/modals/JobCreateModal.svelte'; -import { user } from '$lib/stores/user.store'; +import type { HeaderButtonActionItem } from '$lib/types'; import { handleError } from '$lib/utils/handle-error'; import { getFormatter } from '$lib/utils/i18n'; -import { emptyQueue, getQueue, QueueName, updateQueue, type QueueResponseDto } from '@immich/sdk'; +import { + emptyQueue, + getQueue, + QueueCommand, + QueueName, + runQueueCommandLegacy, + updateQueue, + type QueueResponseDto, +} from '@immich/sdk'; import { modalManager, toastManager, type ActionItem, type IconLike } from '@immich/ui'; import { mdiClose, @@ -23,7 +32,6 @@ import { mdiPlay, mdiPlus, mdiStateMachine, - mdiSync, mdiTable, mdiTagFaces, mdiTrashCanOutline, @@ -31,7 +39,6 @@ import { mdiVideo, } from '@mdi/js'; import type { MessageFormatter } from 'svelte-i18n'; -import { get } from 'svelte/store'; type QueueItem = { icon: IconLike; @@ -39,15 +46,17 @@ type QueueItem = { subtitle?: string; }; -export const getQueuesActions = ($t: MessageFormatter) => { - const ViewQueues: ActionItem = { - title: $t('admin.queues'), - description: $t('admin.queues_page_description'), - icon: mdiSync, - type: $t('page'), - isGlobal: true, - $if: () => get(user)?.isAdmin, - onAction: () => goto(AppRoute.ADMIN_QUEUES), +export const getQueuesActions = ($t: MessageFormatter, queues: QueueResponseDto[] | undefined) => { + const pausedQueues = (queues ?? []).filter(({ isPaused }) => isPaused).map(({ name }) => name); + + const ResumePaused: HeaderButtonActionItem = { + title: $t('resume_paused_jobs', { values: { count: pausedQueues.length } }), + $if: () => pausedQueues.length > 0, + icon: mdiPlay, + onAction: () => handleResumePausedJobs(pausedQueues), + data: { + title: pausedQueues.join(', '), + }, }; const CreateJob: ActionItem = { @@ -68,7 +77,7 @@ export const getQueuesActions = ($t: MessageFormatter) => { onAction: () => goto(`${AppRoute.ADMIN_SETTINGS}?isOpen=job`), }; - return { ViewQueues, ManageConcurrency, CreateJob }; + return { ResumePaused, ManageConcurrency, CreateJob }; }; export const getQueueActions = ($t: MessageFormatter, queue: QueueResponseDto) => { @@ -126,6 +135,19 @@ export const handleEmptyQueue = async (queue: QueueResponseDto) => { } }; +const handleResumePausedJobs = async (queues: QueueName[]) => { + const $t = await getFormatter(); + + try { + for (const name of queues) { + await runQueueCommandLegacy({ name, queueCommandDto: { command: QueueCommand.Resume, force: false } }); + } + await queueManager.refresh(); + } catch (error) { + handleError(error, $t('admin.failed_job_command', { values: { command: 'resume', job: 'paused jobs' } })); + } +}; + const handleRemoveFailedJobs = async (queue: QueueResponseDto) => { const $t = await getFormatter(); diff --git a/web/src/lib/services/shared-link.service.ts b/web/src/lib/services/shared-link.service.ts index 4e6a942682..cbea6ddd9d 100644 --- a/web/src/lib/services/shared-link.service.ts +++ b/web/src/lib/services/shared-link.service.ts @@ -24,26 +24,26 @@ export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLin const Edit: ActionItem = { title: $t('edit_link'), icon: mdiPencilOutline, - onAction: () => void goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`), + onAction: () => goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`), }; const Delete: ActionItem = { title: $t('delete_link'), icon: mdiTrashCanOutline, color: 'danger', - onAction: () => void handleDeleteSharedLink(sharedLink), + onAction: () => handleDeleteSharedLink(sharedLink), }; const Copy: ActionItem = { title: $t('copy_link'), icon: mdiContentCopy, - onAction: () => void copyToClipboard(asUrl(sharedLink)), + onAction: () => copyToClipboard(asUrl(sharedLink)), }; const ViewQrCode: ActionItem = { title: $t('view_qr_code'), icon: mdiQrcode, - onAction: () => void handleShowSharedLinkQrCode(sharedLink), + onAction: () => handleShowSharedLinkQrCode(sharedLink), }; return { Edit, Delete, Copy, ViewQrCode }; @@ -88,7 +88,7 @@ export const handleUpdateSharedLink = async (sharedLink: SharedLinkResponseDto, } }; -export const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto): Promise => { +const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto) => { const $t = await getFormatter(); const success = await modalManager.showDialog({ title: $t('delete_shared_link'), @@ -96,17 +96,15 @@ export const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto): confirmText: $t('delete'), }); if (!success) { - return false; + return; } try { await removeSharedLink({ id: sharedLink.id }); eventManager.emit('SharedLinkDelete', sharedLink); toastManager.success($t('deleted_shared_link')); - return true; } catch (error) { handleError(error, $t('errors.unable_to_delete_shared_link')); - return false; } }; diff --git a/web/src/lib/services/system-config.service.ts b/web/src/lib/services/system-config.service.ts index ffd0094c72..b8c7716d47 100644 --- a/web/src/lib/services/system-config.service.ts +++ b/web/src/lib/services/system-config.service.ts @@ -20,7 +20,7 @@ export const getSystemConfigActions = ( description: $t('admin.copy_config_to_clipboard_description'), type: $t('command'), icon: mdiContentCopy, - onAction: () => void handleCopyToClipboard(config), + onAction: () => handleCopyToClipboard(config), shortcuts: { shift: true, key: 'c' }, }; diff --git a/web/src/lib/services/user-admin.service.ts b/web/src/lib/services/user-admin.service.ts index 7a49f2fbe3..997a43fc7f 100644 --- a/web/src/lib/services/user-admin.service.ts +++ b/web/src/lib/services/user-admin.service.ts @@ -1,11 +1,13 @@ import { goto } from '$app/navigation'; import { eventManager } from '$lib/managers/event-manager.svelte'; +import { serverConfigManager } from '$lib/managers/server-config-manager.svelte'; import PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte'; import UserCreateModal from '$lib/modals/UserCreateModal.svelte'; import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte'; import UserEditModal from '$lib/modals/UserEditModal.svelte'; import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte'; import { user as authUser } from '$lib/stores/user.store'; +import type { HeaderButtonActionItem } from '$lib/types'; import { handleError } from '$lib/utils/handle-error'; import { getFormatter } from '$lib/utils/i18n'; import { @@ -28,6 +30,7 @@ import { mdiPlusBoxOutline, mdiTrashCanOutline, } from '@mdi/js'; +import { DateTime } from 'luxon'; import type { MessageFormatter } from 'svelte-i18n'; import { get } from 'svelte/store'; @@ -36,7 +39,7 @@ export const getUserAdminsActions = ($t: MessageFormatter) => { title: $t('create_user'), type: $t('command'), icon: mdiPlusBoxOutline, - onAction: () => void modalManager.show(UserCreateModal, {}), + onAction: () => modalManager.show(UserCreateModal, {}), shortcuts: { shift: true, key: 'n' }, }; @@ -60,11 +63,17 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons shortcuts: { key: 'Backspace' }, }; - const Restore: ActionItem = { + const getDeleteDate = (deletedAt: string): Date => + DateTime.fromISO(deletedAt).plus({ days: serverConfigManager.value.userDeleteDelay }).toJSDate(); + + const Restore: HeaderButtonActionItem = { icon: mdiDeleteRestore, title: $t('restore'), type: $t('command'), color: 'primary', + data: { + title: $t('admin.user_restore_scheduled_removal', { values: { date: getDeleteDate(user.deletedAt!) } }), + }, $if: () => !!user.deletedAt && user.status === UserStatus.Deleted, onAction: () => modalManager.show(UserRestoreConfirmModal, { user }), }; @@ -74,14 +83,14 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons title: $t('reset_password'), type: $t('command'), $if: () => get(authUser).id !== user.id, - onAction: () => void handleResetPasswordUserAdmin(user), + onAction: () => handleResetPasswordUserAdmin(user), }; const ResetPinCode: ActionItem = { icon: mdiLockSmart, type: $t('command'), title: $t('reset_pin_code'), - onAction: () => void handleResetPinCodeUserAdmin(user), + onAction: () => handleResetPinCodeUserAdmin(user), }; return { Update, Delete, Restore, ResetPassword, ResetPinCode }; @@ -162,12 +171,12 @@ const generatePassword = (length: number = 16) => { return generatedPassword; }; -export const handleResetPasswordUserAdmin = async (user: UserAdminResponseDto) => { +const handleResetPasswordUserAdmin = async (user: UserAdminResponseDto) => { const $t = await getFormatter(); const prompt = $t('admin.confirm_user_password_reset', { values: { user: user.name } }); const success = await modalManager.showDialog({ prompt }); if (!success) { - return false; + return; } try { @@ -176,28 +185,24 @@ export const handleResetPasswordUserAdmin = async (user: UserAdminResponseDto) = eventManager.emit('UserAdminUpdate', response); toastManager.success(); await modalManager.show(PasswordResetSuccessModal, { newPassword: dto.password }); - return true; } catch (error) { handleError(error, $t('errors.unable_to_reset_password')); - return false; } }; -export const handleResetPinCodeUserAdmin = async (user: UserAdminResponseDto) => { +const handleResetPinCodeUserAdmin = async (user: UserAdminResponseDto) => { const $t = await getFormatter(); const prompt = $t('admin.confirm_user_pin_code_reset', { values: { user: user.name } }); const success = await modalManager.showDialog({ prompt }); if (!success) { - return false; + return; } try { const response = await updateUserAdmin({ id: user.id, userAdminUpdateDto: { pinCode: null } }); eventManager.emit('UserAdminUpdate', response); toastManager.success($t('pin_code_reset_successfully')); - return true; } catch (error) { handleError(error, $t('errors.unable_to_reset_pin_code')); - return false; } }; diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index e7d38b1a25..dbe3c851a0 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -1,4 +1,5 @@ import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk'; +import type { ActionItem } from '@immich/ui'; export interface ReleaseEvent { isAvailable: boolean; @@ -9,3 +10,5 @@ export interface ReleaseEvent { } export type QueueSnapshot = { timestamp: number; snapshot?: QueueResponseDto[] }; + +export type HeaderButtonActionItem = ActionItem & { data?: { title?: string } }; diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index c8f41b6fbc..77a3d402b2 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -14,15 +14,15 @@ import { themeManager } from '$lib/managers/theme-manager.svelte'; import ServerRestartingModal from '$lib/modals/ServerRestartingModal.svelte'; import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte'; - import { getQueuesActions } from '$lib/services/queue.service'; + import { sidebarStore } from '$lib/stores/sidebar.svelte'; import { user } from '$lib/stores/user.store'; import { closeWebsocketConnection, openWebsocketConnection, websocketStore } from '$lib/stores/websocket'; import type { ReleaseEvent } from '$lib/types'; import { copyToClipboard, getReleaseType, semverToName } from '$lib/utils'; import { maintenanceShouldRedirect } from '$lib/utils/maintenance'; import { isAssetViewerRoute } from '$lib/utils/navigation'; - import { CommandPaletteContext, modalManager, setTranslations, type ActionItem } from '@immich/ui'; - import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiThemeLightDark } from '@mdi/js'; + import { CommandPaletteContext, modalManager, setTranslations, toastManager, type ActionItem } from '@immich/ui'; + import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiThemeLightDark } from '@mdi/js'; import { onMount, type Snippet } from 'svelte'; import { t } from 'svelte-i18n'; import '../app.css'; @@ -53,6 +53,8 @@ return new URL(page.url.pathname + page.url.search, 'https://my.immich.app'); }; + toastManager.setOptions({ class: 'top-16' }); + onMount(() => { const element = document.querySelector('#stencil'); element?.remove(); @@ -62,6 +64,10 @@ eventManager.emit('AppInit'); beforeNavigate(({ from, to }) => { + if (sidebarStore.isOpen) { + sidebarStore.reset(); + } + if (isAssetViewerRoute(from) && isAssetViewerRoute(to)) { return; } @@ -149,6 +155,13 @@ icon: mdiCog, onAction: () => goto(AppRoute.ADMIN_SETTINGS), }, + { + title: $t('admin.queues'), + description: $t('admin.queues_page_description'), + icon: mdiSync, + type: $t('page'), + onAction: () => goto(AppRoute.ADMIN_QUEUES), + }, { title: $t('external_libraries'), description: $t('admin.external_libraries_page_description'), @@ -163,7 +176,7 @@ }, ].map((route) => ({ ...route, type: $t('page'), isGlobal: true, $if: () => $user?.isAdmin })); - const commands = $derived([...userCommands, ...adminCommands, ...Object.values(getQueuesActions($t))]); + const commands = $derived([...userCommands, ...adminCommands]); diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index aef8447d00..9aa5af6481 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -1,6 +1,5 @@ - {#snippet buttons()} - - - - - - - - {/snippet}
{#if user.deletedAt}