mirror of
https://github.com/immich-app/immich.git
synced 2025-12-23 09:15:05 +03:00
Merge branch 'main' into feature/rearrange-buttons-2
This commit is contained in:
2
.github/package.json
vendored
2
.github/package.json
vendored
@@ -4,6 +4,6 @@
|
|||||||
"format:fix": "prettier --write ."
|
"format:fix": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.5.3"
|
"prettier": "^3.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
.github/workflows/build-mobile.yml
vendored
8
.github/workflows/build-mobile.yml
vendored
@@ -222,6 +222,7 @@ jobs:
|
|||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.3'
|
ruby-version: '3.3'
|
||||||
|
bundler-cache: true
|
||||||
working-directory: ./mobile/ios
|
working-directory: ./mobile/ios
|
||||||
|
|
||||||
- name: Install CocoaPods dependencies
|
- name: Install CocoaPods dependencies
|
||||||
@@ -229,13 +230,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pod install
|
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
|
- name: Create API Key
|
||||||
env:
|
env:
|
||||||
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
"eslint-plugin-unicorn": "^62.0.0",
|
"eslint-plugin-unicorn": "^62.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "^8.28.0",
|
"typescript-eslint": "^8.28.0",
|
||||||
|
|||||||
@@ -58,10 +58,6 @@ services:
|
|||||||
IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues
|
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_DOCUMENTATION_URL: https://docs.immich.app
|
||||||
IMMICH_THIRD_PARTY_SUPPORT_URL: https://docs.immich.app/community-guides
|
IMMICH_THIRD_PARTY_SUPPORT_URL: https://docs.immich.app/community-guides
|
||||||
ulimits:
|
|
||||||
nofile:
|
|
||||||
soft: 1048576
|
|
||||||
hard: 1048576
|
|
||||||
ports:
|
ports:
|
||||||
- 9230:9230
|
- 9230:9230
|
||||||
- 9231:9231
|
- 9231:9231
|
||||||
@@ -100,10 +96,6 @@ services:
|
|||||||
- app-node_modules:/usr/src/app/node_modules
|
- app-node_modules:/usr/src/app/node_modules
|
||||||
- sveltekit:/usr/src/app/web/.svelte-kit
|
- sveltekit:/usr/src/app/web/.svelte-kit
|
||||||
- coverage:/usr/src/app/web/coverage
|
- coverage:/usr/src/app/web/coverage
|
||||||
ulimits:
|
|
||||||
nofile:
|
|
||||||
soft: 1048576
|
|
||||||
hard: 1048576
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
immich-server:
|
immich-server:
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ server {
|
|||||||
|
|
||||||
# enable websockets: http://nginx.org/en/docs/http/websocket.html
|
# enable websockets: http://nginx.org/en/docs/http/websocket.html
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
|
|
||||||
# set timeout
|
# set timeout
|
||||||
@@ -43,6 +41,8 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://<backend_url>:2283;
|
proxy_pass http://<backend_url>:2283;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
}
|
}
|
||||||
|
|
||||||
# useful when using Let's Encrypt http-01 challenge
|
# useful when using Let's Encrypt http-01 challenge
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"@docusaurus/module-type-aliases": "~3.9.0",
|
"@docusaurus/module-type-aliases": "~3.9.0",
|
||||||
"@docusaurus/tsconfig": "^3.7.0",
|
"@docusaurus/tsconfig": "^3.7.0",
|
||||||
"@docusaurus/types": "^3.7.0",
|
"@docusaurus/types": "^3.7.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
|||||||
@@ -36,14 +36,14 @@
|
|||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^62.0.0",
|
"eslint-plugin-unicorn": "^62.0.0",
|
||||||
"exiftool-vendored": "^33.0.0",
|
"exiftool-vendored": "^34.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jose": "^5.6.3",
|
"jose": "^5.6.3",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"oidc-provider": "^9.0.0",
|
"oidc-provider": "^9.0.0",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
|
|||||||
@@ -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.",
|
"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",
|
"export_config_as_json_description": "Download the current system config as a JSON file",
|
||||||
"external_libraries_page_description": "Admin external library page",
|
"external_libraries_page_description": "Admin external library page",
|
||||||
"external_library_management": "External Library Management",
|
|
||||||
"face_detection": "Face detection",
|
"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.",
|
"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.",
|
"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.",
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class DriftActivitiesPage extends HookConsumerWidget {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(album.name),
|
title: Text(album.name),
|
||||||
actions: [const LikeActivityActionButton(menuItem: true)],
|
actions: [const LikeActivityActionButton(iconOnly: true)],
|
||||||
actionsPadding: const EdgeInsets.only(right: 8),
|
actionsPadding: const EdgeInsets.only(right: 8),
|
||||||
),
|
),
|
||||||
body: activities.widgetWhen(
|
body: activities.widgetWhen(
|
||||||
|
|||||||
@@ -27,8 +27,19 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
|||||||
bool isAlbumTitleTextFieldFocus = false;
|
bool isAlbumTitleTextFieldFocus = false;
|
||||||
Set<BaseAsset> selectedAssets = {};
|
Set<BaseAsset> selectedAssets = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
albumTitleController.addListener(_onTitleChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTitleChanged() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
albumTitleController.removeListener(_onTitleChanged);
|
||||||
albumTitleController.dispose();
|
albumTitleController.dispose();
|
||||||
albumDescriptionController.dispose();
|
albumDescriptionController.dispose();
|
||||||
albumTitleTextFieldFocusNode.dispose();
|
albumTitleTextFieldFocusNode.dispose();
|
||||||
|
|||||||
@@ -21,12 +21,36 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee
|
|||||||
|
|
||||||
enum AddToMenuItem { album, archive, unarchive, lockedFolder }
|
enum AddToMenuItem { album, archive, unarchive, lockedFolder }
|
||||||
|
|
||||||
class AddActionButton extends ConsumerWidget {
|
class AddActionButton extends ConsumerStatefulWidget {
|
||||||
const AddActionButton({super.key});
|
const AddActionButton({super.key, this.originalTheme});
|
||||||
|
|
||||||
Future<void> _showAddOptions(BuildContext context, WidgetRef ref) async {
|
final ThemeData? originalTheme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<AddActionButton> createState() => _AddActionButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||||
|
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<Widget> _buildMenuChildren() {
|
||||||
final asset = ref.read(currentAssetNotifier);
|
final asset = ref.read(currentAssetNotifier);
|
||||||
if (asset == null) return;
|
if (asset == null) return [];
|
||||||
|
|
||||||
final user = ref.read(currentUserProvider);
|
final user = ref.read(currentUserProvider);
|
||||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||||
@@ -35,93 +59,57 @@ class AddActionButton extends ConsumerWidget {
|
|||||||
final hasRemote = asset is RemoteAsset;
|
final hasRemote = asset is RemoteAsset;
|
||||||
final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived;
|
final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived;
|
||||||
final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived;
|
final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived;
|
||||||
final menuItemHeight = 30.0;
|
|
||||||
|
|
||||||
final List<PopupMenuEntry<AddToMenuItem>> items = [
|
return [
|
||||||
PopupMenuItem(
|
Padding(
|
||||||
enabled: false,
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
textStyle: context.textTheme.labelMedium,
|
child: Text("add_to_bottom_bar".tr(), style: context.textTheme.labelMedium),
|
||||||
height: 40,
|
|
||||||
child: Text("add_to_bottom_bar".tr()),
|
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
BaseActionButton(
|
||||||
height: menuItemHeight,
|
iconData: Icons.photo_album_outlined,
|
||||||
value: AddToMenuItem.album,
|
label: "album".tr(),
|
||||||
child: ListTile(leading: const Icon(Icons.photo_album_outlined), title: Text("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) ...[
|
if (isOwner) ...[
|
||||||
|
const Divider(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Text("move_to".tr(), style: context.textTheme.labelMedium),
|
||||||
|
),
|
||||||
if (showArchive)
|
if (showArchive)
|
||||||
PopupMenuItem(
|
BaseActionButton(
|
||||||
height: menuItemHeight,
|
iconData: Icons.archive_outlined,
|
||||||
value: AddToMenuItem.archive,
|
label: "archive".tr(),
|
||||||
child: ListTile(leading: const Icon(Icons.archive_outlined), title: Text("archive".tr())),
|
menuItem: true,
|
||||||
|
onPressed: () => _handleMenuSelection(AddToMenuItem.archive),
|
||||||
),
|
),
|
||||||
if (showUnarchive)
|
if (showUnarchive)
|
||||||
PopupMenuItem(
|
BaseActionButton(
|
||||||
height: menuItemHeight,
|
iconData: Icons.unarchive_outlined,
|
||||||
value: AddToMenuItem.unarchive,
|
label: "unarchive".tr(),
|
||||||
child: ListTile(leading: const Icon(Icons.unarchive_outlined), title: Text("unarchive".tr())),
|
menuItem: true,
|
||||||
|
onPressed: () => _handleMenuSelection(AddToMenuItem.unarchive),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
BaseActionButton(
|
||||||
height: menuItemHeight,
|
iconData: Icons.lock_outline,
|
||||||
value: AddToMenuItem.lockedFolder,
|
label: "locked_folder".tr(),
|
||||||
child: ListTile(leading: const Icon(Icons.lock_outline), title: Text("locked_folder".tr())),
|
menuItem: true,
|
||||||
|
onPressed: () => _handleMenuSelection(AddToMenuItem.lockedFolder),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
final AddToMenuItem? selected = await showMenu<AddToMenuItem>(
|
|
||||||
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) {
|
void _openAlbumSelector() {
|
||||||
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) {
|
|
||||||
final currentAsset = ref.read(currentAssetNotifier);
|
final currentAsset = ref.read(currentAssetNotifier);
|
||||||
if (currentAsset == null) {
|
if (currentAsset == null) {
|
||||||
ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error);
|
ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Widget> slivers = [
|
final List<Widget> slivers = [AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album))];
|
||||||
AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(context, ref, album)),
|
|
||||||
];
|
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -141,7 +129,7 @@ class AddActionButton extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addCurrentAssetToAlbum(BuildContext context, WidgetRef ref, RemoteAlbum album) async {
|
Future<void> _addCurrentAssetToAlbum(RemoteAlbum album) async {
|
||||||
final latest = ref.read(currentAssetNotifier);
|
final latest = ref.read(currentAssetNotifier);
|
||||||
|
|
||||||
if (latest == null) {
|
if (latest == null) {
|
||||||
@@ -174,17 +162,36 @@ class AddActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
final asset = ref.watch(currentAssetNotifier);
|
final asset = ref.watch(currentAssetNotifier);
|
||||||
if (asset == null) {
|
if (asset == null) {
|
||||||
return const SizedBox.shrink();
|
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(
|
return BaseActionButton(
|
||||||
iconData: Icons.add,
|
iconData: Icons.add,
|
||||||
label: "add_to_bottom_bar".tr(),
|
label: "add_to_bottom_bar".tr(),
|
||||||
onPressed: () => _showAddOptions(buttonContext, ref),
|
onPressed: () => controller.isOpen ? controller.close() : controller.open(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class BaseActionButton extends StatelessWidget {
|
|||||||
this.onLongPressed,
|
this.onLongPressed,
|
||||||
this.maxWidth = 90.0,
|
this.maxWidth = 90.0,
|
||||||
this.minWidth,
|
this.minWidth,
|
||||||
|
this.iconOnly = false,
|
||||||
this.menuItem = false,
|
this.menuItem = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -19,6 +20,11 @@ class BaseActionButton extends StatelessWidget {
|
|||||||
final Color? iconColor;
|
final Color? iconColor;
|
||||||
final double maxWidth;
|
final double maxWidth;
|
||||||
final double? minWidth;
|
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 bool menuItem;
|
||||||
final void Function()? onPressed;
|
final void Function()? onPressed;
|
||||||
final void Function()? onLongPressed;
|
final void Function()? onLongPressed;
|
||||||
@@ -31,33 +37,23 @@ class BaseActionButton extends StatelessWidget {
|
|||||||
final iconColor = this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color;
|
final iconColor = this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color;
|
||||||
final textColor = context.themeData.textTheme.labelLarge?.color;
|
final textColor = context.themeData.textTheme.labelLarge?.color;
|
||||||
|
|
||||||
if (menuItem) {
|
if (iconOnly) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
icon: Icon(iconData, size: iconSize, color: iconColor),
|
icon: Icon(iconData, size: iconSize, color: iconColor),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.findAncestorWidgetOfExactType<MenuAnchor>() != null) {
|
if (menuItem) {
|
||||||
final theme = context.themeData;
|
final theme = context.themeData;
|
||||||
final textStyle = theme.textTheme.bodyMedium;
|
final effectiveStyle = theme.textTheme.labelLarge;
|
||||||
final defaultTextColor = theme.colorScheme.onSurfaceVariant;
|
|
||||||
final effectiveStyle = (textStyle ?? theme.textTheme.bodyMedium)?.copyWith(
|
|
||||||
color: (textStyle?.color ?? defaultTextColor),
|
|
||||||
);
|
|
||||||
final effectiveIconColor = iconColor ?? theme.iconTheme.color ?? theme.colorScheme.onSurfaceVariant;
|
final effectiveIconColor = iconColor ?? theme.iconTheme.color ?? theme.colorScheme.onSurfaceVariant;
|
||||||
|
|
||||||
return MenuItemButton(
|
return MenuItemButton(
|
||||||
style: MenuItemButton.styleFrom(
|
style: MenuItemButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12)),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
leadingIcon: Icon(iconData, color: effectiveIconColor, size: 20),
|
||||||
visualDensity: const VisualDensity(vertical: -2.5),
|
|
||||||
),
|
|
||||||
trailingIcon: Icon(iconData, size: 18, color: effectiveIconColor),
|
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: Align(
|
child: Text(label, style: effectiveStyle),
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(label, style: effectiveStyle),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import 'package:immich_mobile/providers/cast.provider.dart';
|
|||||||
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
||||||
|
|
||||||
class CastActionButton extends ConsumerWidget {
|
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;
|
final bool menuItem;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -22,6 +23,7 @@ class CastActionButton extends ConsumerWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(context: context, builder: (context) => const CastDialog());
|
showDialog(context: context, builder: (context) => const CastDialog());
|
||||||
},
|
},
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
|||||||
|
|
||||||
class DownloadActionButton extends ConsumerWidget {
|
class DownloadActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
final ActionSource source;
|
||||||
|
final bool iconOnly;
|
||||||
final bool menuItem;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -38,6 +39,7 @@ class DownloadActionButton extends ConsumerWidget {
|
|||||||
iconData: Icons.download,
|
iconData: Icons.download,
|
||||||
maxWidth: 95,
|
maxWidth: 95,
|
||||||
label: "download".t(context: context),
|
label: "download".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref, backgroundManager),
|
onPressed: () => _onTap(context, ref, backgroundManager),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|||||||
|
|
||||||
class FavoriteActionButton extends ConsumerWidget {
|
class FavoriteActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
final ActionSource source;
|
||||||
|
final bool iconOnly;
|
||||||
final bool menuItem;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -44,6 +45,7 @@ class FavoriteActionButton extends ConsumerWidget {
|
|||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.favorite_border_rounded,
|
iconData: Icons.favorite_border_rounded,
|
||||||
label: "favorite".t(context: context),
|
label: "favorite".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
|||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
class LikeActivityActionButton extends ConsumerWidget {
|
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;
|
final bool menuItem;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -49,6 +50,7 @@ class LikeActivityActionButton extends ConsumerWidget {
|
|||||||
iconData: liked != null ? Icons.favorite : Icons.favorite_border,
|
iconData: liked != null ? Icons.favorite : Icons.favorite_border,
|
||||||
label: "like".t(context: context),
|
label: "like".t(context: context),
|
||||||
onPressed: () => onTap(liked),
|
onPressed: () => onTap(liked),
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -57,6 +59,7 @@ class LikeActivityActionButton extends ConsumerWidget {
|
|||||||
loading: () => BaseActionButton(
|
loading: () => BaseActionButton(
|
||||||
iconData: Icons.favorite_border,
|
iconData: Icons.favorite_border,
|
||||||
label: "like".t(context: context),
|
label: "like".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
),
|
),
|
||||||
error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])),
|
error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])),
|
||||||
|
|||||||
@@ -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';
|
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||||
|
|
||||||
class MotionPhotoActionButton extends ConsumerWidget {
|
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;
|
final bool menuItem;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -17,6 +18,7 @@ class MotionPhotoActionButton extends ConsumerWidget {
|
|||||||
iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded,
|
iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded,
|
||||||
label: "play_motion_photo".t(context: context),
|
label: "play_motion_photo".t(context: context),
|
||||||
onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle,
|
onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle,
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|||||||
|
|
||||||
class UnFavoriteActionButton extends ConsumerWidget {
|
class UnFavoriteActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
final ActionSource source;
|
||||||
|
final bool iconOnly;
|
||||||
final bool menuItem;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -45,6 +46,7 @@ class UnFavoriteActionButton extends ConsumerWidget {
|
|||||||
iconData: Icons.favorite_rounded,
|
iconData: Icons.favorite_rounded,
|
||||||
label: "unfavorite".t(context: context),
|
label: "unfavorite".t(context: context),
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
|
if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||||
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
|
if (isCasting || (asset.hasRemote)) const CastActionButton(iconOnly: true),
|
||||||
if (album != null && album.isActivityEnabled && album.isShared)
|
if (album != null && album.isActivityEnabled && album.isShared)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.chat_outlined),
|
icon: const Icon(Icons.chat_outlined),
|
||||||
@@ -102,16 +102,16 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
tooltip: 'view_in_timeline'.t(context: context),
|
tooltip: 'view_in_timeline'.t(context: context),
|
||||||
),
|
),
|
||||||
if (asset.hasRemote && isOwner && !asset.isFavorite)
|
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)
|
if (asset.hasRemote && isOwner && asset.isFavorite)
|
||||||
const UnFavoriteActionButton(source: ActionSource.viewer, menuItem: true),
|
const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
|
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||||
ViewerKebabMenu(onConfigureButtons: openConfigurator),
|
const ViewerKebabMenu(),
|
||||||
];
|
];
|
||||||
|
|
||||||
final lockedViewActions = <Widget>[
|
final lockedViewActions = <Widget>[
|
||||||
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
|
if (isCasting || (asset.hasRemote)) const CastActionButton(iconOnly: true),
|
||||||
ViewerKebabMenu(onConfigureButtons: openConfigurator),
|
const ViewerKebabMenu(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ class AppRouter extends RootStackRouter {
|
|||||||
AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]),
|
AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]),
|
||||||
AutoRoute(page: ChangePasswordRoute.page),
|
AutoRoute(page: ChangePasswordRoute.page),
|
||||||
AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false),
|
AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false),
|
||||||
CustomRoute(
|
AutoRoute(
|
||||||
page: TabControllerRoute.page,
|
page: TabControllerRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
children: [
|
children: [
|
||||||
@@ -176,9 +176,8 @@ class AppRouter extends RootStackRouter {
|
|||||||
AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
],
|
],
|
||||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
|
||||||
),
|
),
|
||||||
CustomRoute(
|
AutoRoute(
|
||||||
page: TabShellRoute.page,
|
page: TabShellRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
children: [
|
children: [
|
||||||
@@ -187,7 +186,6 @@ class AppRouter extends RootStackRouter {
|
|||||||
AutoRoute(page: DriftLibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: DriftLibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
],
|
],
|
||||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
|
||||||
),
|
),
|
||||||
CustomRoute(
|
CustomRoute(
|
||||||
page: GalleryViewerRoute.page,
|
page: GalleryViewerRoute.page,
|
||||||
|
|||||||
117
pnpm-lock.yaml
generated
117
pnpm-lock.yaml
generated
@@ -20,8 +20,8 @@ importers:
|
|||||||
.github:
|
.github:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.5.3
|
specifier: ^3.7.4
|
||||||
version: 3.7.1
|
version: 3.7.4
|
||||||
|
|
||||||
cli:
|
cli:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -85,7 +85,7 @@ importers:
|
|||||||
version: 10.1.8(eslint@9.39.1(jiti@2.6.1))
|
version: 10.1.8(eslint@9.39.1(jiti@2.6.1))
|
||||||
eslint-plugin-prettier:
|
eslint-plugin-prettier:
|
||||||
specifier: ^5.1.3
|
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:
|
eslint-plugin-unicorn:
|
||||||
specifier: ^62.0.0
|
specifier: ^62.0.0
|
||||||
version: 62.0.0(eslint@9.39.1(jiti@2.6.1))
|
version: 62.0.0(eslint@9.39.1(jiti@2.6.1))
|
||||||
@@ -96,11 +96,11 @@ importers:
|
|||||||
specifier: ^5.2.0
|
specifier: ^5.2.0
|
||||||
version: 5.5.0
|
version: 5.5.0
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.2.5
|
specifier: ^3.7.4
|
||||||
version: 3.7.1
|
version: 3.7.4
|
||||||
prettier-plugin-organize-imports:
|
prettier-plugin-organize-imports:
|
||||||
specifier: ^4.0.0
|
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:
|
typescript:
|
||||||
specifier: ^5.3.3
|
specifier: ^5.3.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -184,8 +184,8 @@ importers:
|
|||||||
specifier: ^3.7.0
|
specifier: ^3.7.0
|
||||||
version: 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.2.4
|
specifier: ^3.7.4
|
||||||
version: 3.7.1
|
version: 3.7.4
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.1.6
|
specifier: ^5.1.6
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -239,13 +239,13 @@ importers:
|
|||||||
version: 10.1.8(eslint@9.39.1(jiti@2.6.1))
|
version: 10.1.8(eslint@9.39.1(jiti@2.6.1))
|
||||||
eslint-plugin-prettier:
|
eslint-plugin-prettier:
|
||||||
specifier: ^5.1.3
|
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:
|
eslint-plugin-unicorn:
|
||||||
specifier: ^62.0.0
|
specifier: ^62.0.0
|
||||||
version: 62.0.0(eslint@9.39.1(jiti@2.6.1))
|
version: 62.0.0(eslint@9.39.1(jiti@2.6.1))
|
||||||
exiftool-vendored:
|
exiftool-vendored:
|
||||||
specifier: ^33.0.0
|
specifier: ^34.0.0
|
||||||
version: 33.5.0
|
version: 34.0.0
|
||||||
globals:
|
globals:
|
||||||
specifier: ^16.0.0
|
specifier: ^16.0.0
|
||||||
version: 16.5.0
|
version: 16.5.0
|
||||||
@@ -265,11 +265,11 @@ importers:
|
|||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.0.0
|
version: 7.0.0
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.2.5
|
specifier: ^3.7.4
|
||||||
version: 3.7.1
|
version: 3.7.4
|
||||||
prettier-plugin-organize-imports:
|
prettier-plugin-organize-imports:
|
||||||
specifier: ^4.0.0
|
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:
|
sharp:
|
||||||
specifier: ^0.34.5
|
specifier: ^0.34.5
|
||||||
version: 0.34.5
|
version: 0.34.5
|
||||||
@@ -428,8 +428,8 @@ importers:
|
|||||||
specifier: 4.3.3
|
specifier: 4.3.3
|
||||||
version: 4.3.3
|
version: 4.3.3
|
||||||
exiftool-vendored:
|
exiftool-vendored:
|
||||||
specifier: ^33.0.0
|
specifier: ^34.0.0
|
||||||
version: 33.5.0
|
version: 34.0.0
|
||||||
express:
|
express:
|
||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.2.0
|
version: 5.2.0
|
||||||
@@ -655,7 +655,7 @@ importers:
|
|||||||
version: 10.1.8(eslint@9.39.1(jiti@2.6.1))
|
version: 10.1.8(eslint@9.39.1(jiti@2.6.1))
|
||||||
eslint-plugin-prettier:
|
eslint-plugin-prettier:
|
||||||
specifier: ^5.1.3
|
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:
|
eslint-plugin-unicorn:
|
||||||
specifier: ^62.0.0
|
specifier: ^62.0.0
|
||||||
version: 62.0.0(eslint@9.39.1(jiti@2.6.1))
|
version: 62.0.0(eslint@9.39.1(jiti@2.6.1))
|
||||||
@@ -672,11 +672,11 @@ importers:
|
|||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.0.0
|
version: 7.0.0
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.0.2
|
specifier: ^3.7.4
|
||||||
version: 3.7.1
|
version: 3.7.4
|
||||||
prettier-plugin-organize-imports:
|
prettier-plugin-organize-imports:
|
||||||
specifier: ^4.0.0
|
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:
|
sql-formatter:
|
||||||
specifier: ^15.0.0
|
specifier: ^15.0.0
|
||||||
version: 15.6.10
|
version: 15.6.10
|
||||||
@@ -717,8 +717,8 @@ importers:
|
|||||||
specifier: file:../open-api/typescript-sdk
|
specifier: file:../open-api/typescript-sdk
|
||||||
version: link:../open-api/typescript-sdk
|
version: link:../open-api/typescript-sdk
|
||||||
'@immich/ui':
|
'@immich/ui':
|
||||||
specifier: ^0.49.2
|
specifier: ^0.50.0
|
||||||
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)
|
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':
|
'@mapbox/mapbox-gl-rtl-text':
|
||||||
specifier: 0.2.3
|
specifier: 0.2.3
|
||||||
version: 0.2.3(mapbox-gl@1.13.3)
|
version: 0.2.3(mapbox-gl@1.13.3)
|
||||||
@@ -904,17 +904,17 @@ importers:
|
|||||||
specifier: ^16.0.0
|
specifier: ^16.0.0
|
||||||
version: 16.5.0
|
version: 16.5.0
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.4.2
|
specifier: ^3.7.4
|
||||||
version: 3.7.1
|
version: 3.7.4
|
||||||
prettier-plugin-organize-imports:
|
prettier-plugin-organize-imports:
|
||||||
specifier: ^4.0.0
|
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:
|
prettier-plugin-sort-json:
|
||||||
specifier: ^4.1.1
|
specifier: ^4.1.1
|
||||||
version: 4.1.1(prettier@3.7.1)
|
version: 4.1.1(prettier@3.7.4)
|
||||||
prettier-plugin-svelte:
|
prettier-plugin-svelte:
|
||||||
specifier: ^3.3.3
|
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:
|
rollup-plugin-visualizer:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.5(rollup@4.53.3)
|
version: 6.0.5(rollup@4.53.3)
|
||||||
@@ -2989,8 +2989,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
|
|
||||||
'@immich/ui@0.49.3':
|
'@immich/ui@0.50.0':
|
||||||
resolution: {integrity: sha512-joqT72Y6gmGK6z25Suzr2VhYANrLo43g20T4UHmbQenz/z/Ax6sl1Ao9SjIOwEkKMm9N3Txoh7WOOzmHVl04OA==}
|
resolution: {integrity: sha512-7AW9SRZTAgal8xlkUAxm7o4+pSG7HcKb+Bh9JpWLaDRRdGyPCZMmsNa9CjZglOQ7wkAD07tQ9u4+zezBLe0dlQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
|
|
||||||
@@ -3236,6 +3236,7 @@ packages:
|
|||||||
'@koa/router@14.0.0':
|
'@koa/router@14.0.0':
|
||||||
resolution: {integrity: sha512-LBSu5K0qAaaQcXX/0WIB9PGDevyCxxpnc1uq13vV/CgObaVxuis5hKl3Eboq/8gcb6ebnkAStW9NB/Em2eYyFA==}
|
resolution: {integrity: sha512-LBSu5K0qAaaQcXX/0WIB9PGDevyCxxpnc1uq13vV/CgObaVxuis5hKl3Eboq/8gcb6ebnkAStW9NB/Em2eYyFA==}
|
||||||
engines: {node: '>= 20'}
|
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':
|
'@koddsson/eslint-plugin-tscompat@0.2.0':
|
||||||
resolution: {integrity: sha512-Oqd4kWSX0LiO9wWHjcmDfXZNC7TotFV/tLRhwCFU3XUeb//KYvJ75c9OmeSJ+vBv5lkCeB+xYsqyNrBc5j18XA==}
|
resolution: {integrity: sha512-Oqd4kWSX0LiO9wWHjcmDfXZNC7TotFV/tLRhwCFU3XUeb//KYvJ75c9OmeSJ+vBv5lkCeB+xYsqyNrBc5j18XA==}
|
||||||
@@ -5503,8 +5504,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==}
|
resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
batch-cluster@15.0.1:
|
batch-cluster@16.0.0:
|
||||||
resolution: {integrity: sha512-eUmh0ld1AUPKTEmdzwGF9QTSexXAyt9rA1F5zDfW1wUi3okA3Tal4NLdCeFI6aiKpBenQhR6NmK9bW9tBHTGPQ==}
|
resolution: {integrity: sha512-+T7Ho09ikx/kP4P8M+GEnpuePzRQa4gTUhtPIu6ApFC8+0GY0sri1y1PuB+yfXlQWl5DkHC/e58z3U6g0qCz/A==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
batch@0.6.1:
|
batch@0.6.1:
|
||||||
@@ -6848,17 +6849,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
exiftool-vendored.exe@13.42.0:
|
exiftool-vendored.exe@13.43.0:
|
||||||
resolution: {integrity: sha512-6AFybe5IakduMWleuQBfep9OWGSVZSedt2uKL+LzufRsATp+beOF7tZyKtMztjb6VRH1GF/4F9EvBVam6zm70w==}
|
resolution: {integrity: sha512-EENHNz86tYY5yHGPtGB2mto3FIGstQvEhrcU34f7fm4RMxBKNfTWYOGkhU1jzvjOi+V4575LQX/FUES1TwgUbQ==}
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
exiftool-vendored.pl@13.42.0:
|
exiftool-vendored.pl@13.43.0:
|
||||||
resolution: {integrity: sha512-EF5IdxQNIJIvZjHf4bG4jnwAHVVSLkYZToo2q+Mm89kSuppKfRvHz/lngIxN0JALE8rFdC4zt6NWY/PKqRdCcg==}
|
resolution: {integrity: sha512-0ApWaQ/pxaliPK7HzTxVA0sg/wZ8vl7UtFVhCyWhGQg01WfZkFrKwKmELB0Bnn01WTfgIuMadba8ccmFvpmJag==}
|
||||||
os: ['!win32']
|
os: ['!win32']
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
exiftool-vendored@33.5.0:
|
exiftool-vendored@34.0.0:
|
||||||
resolution: {integrity: sha512-7cCh6izwdmC5ZaCxpHFehnExIr2Yp7CJuxHg4WFiGcm81yyxXLtvSE+85ep9VsNwhlOtSpk+XxiqrlddjY5lAw==}
|
resolution: {integrity: sha512-rhIe4XGE7kh76nwytwHtq6qK/pc1mpOBHRV++gudFeG2PfAp3XIVQbFWCLK3S4l9I4AWYOe4mxk8mW8l1oHRTw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
expect-type@1.2.1:
|
expect-type@1.2.1:
|
||||||
@@ -9765,8 +9766,8 @@ packages:
|
|||||||
prettier: ^3.0.0
|
prettier: ^3.0.0
|
||||||
svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0
|
svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0
|
||||||
|
|
||||||
prettier@3.7.1:
|
prettier@3.7.4:
|
||||||
resolution: {integrity: sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ==}
|
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -14517,7 +14518,7 @@ snapshots:
|
|||||||
'@fig/complete-commander@3.2.0(commander@11.1.0)':
|
'@fig/complete-commander@3.2.0(commander@11.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
commander: 11.1.0
|
commander: 11.1.0
|
||||||
prettier: 3.7.1
|
prettier: 3.7.4
|
||||||
|
|
||||||
'@floating-ui/core@1.7.3':
|
'@floating-ui/core@1.7.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -14700,7 +14701,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
svelte: 5.45.2
|
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:
|
dependencies:
|
||||||
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.45.2)
|
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.45.2)
|
||||||
'@internationalized/date': 3.10.0
|
'@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)':
|
'@react-email/render@1.4.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
html-to-text: 9.0.5
|
html-to-text: 9.0.5
|
||||||
prettier: 3.7.1
|
prettier: 3.7.4
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
react-dom: 19.2.0(react@19.2.0)
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
react-promise-suspense: 0.3.4
|
react-promise-suspense: 0.3.4
|
||||||
@@ -17580,7 +17581,7 @@ snapshots:
|
|||||||
|
|
||||||
baseline-browser-mapping@2.8.31: {}
|
baseline-browser-mapping@2.8.31: {}
|
||||||
|
|
||||||
batch-cluster@15.0.1: {}
|
batch-cluster@16.0.0: {}
|
||||||
|
|
||||||
batch@0.6.1: {}
|
batch@0.6.1: {}
|
||||||
|
|
||||||
@@ -18907,10 +18908,10 @@ snapshots:
|
|||||||
lodash.memoize: 4.1.2
|
lodash.memoize: 4.1.2
|
||||||
semver: 7.7.3
|
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:
|
dependencies:
|
||||||
eslint: 9.39.1(jiti@2.6.1)
|
eslint: 9.39.1(jiti@2.6.1)
|
||||||
prettier: 3.7.1
|
prettier: 3.7.4
|
||||||
prettier-linter-helpers: 1.0.0
|
prettier-linter-helpers: 1.0.0
|
||||||
synckit: 0.11.11
|
synckit: 0.11.11
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -19128,21 +19129,21 @@ snapshots:
|
|||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
strip-final-newline: 2.0.0
|
strip-final-newline: 2.0.0
|
||||||
|
|
||||||
exiftool-vendored.exe@13.42.0:
|
exiftool-vendored.exe@13.43.0:
|
||||||
optional: true
|
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:
|
dependencies:
|
||||||
'@photostructure/tz-lookup': 11.3.0
|
'@photostructure/tz-lookup': 11.3.0
|
||||||
'@types/luxon': 3.7.1
|
'@types/luxon': 3.7.1
|
||||||
batch-cluster: 15.0.1
|
batch-cluster: 16.0.0
|
||||||
exiftool-vendored.pl: 13.42.0
|
exiftool-vendored.pl: 13.43.0
|
||||||
he: 1.2.0
|
he: 1.2.0
|
||||||
luxon: 3.7.2
|
luxon: 3.7.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
exiftool-vendored.exe: 13.42.0
|
exiftool-vendored.exe: 13.43.0
|
||||||
|
|
||||||
expect-type@1.2.1: {}
|
expect-type@1.2.1: {}
|
||||||
|
|
||||||
@@ -22636,21 +22637,21 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fast-diff: 1.3.0
|
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:
|
dependencies:
|
||||||
prettier: 3.7.1
|
prettier: 3.7.4
|
||||||
typescript: 5.9.3
|
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:
|
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:
|
dependencies:
|
||||||
prettier: 3.7.1
|
prettier: 3.7.4
|
||||||
svelte: 5.45.2
|
svelte: 5.45.2
|
||||||
|
|
||||||
prettier@3.7.1: {}
|
prettier@3.7.4: {}
|
||||||
|
|
||||||
pretty-error@4.0.0:
|
pretty-error@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -50,13 +50,15 @@ RUN --mount=type=cache,id=pnpm-cli,target=/buildcache/pnpm-store \
|
|||||||
|
|
||||||
FROM builder AS plugins
|
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
|
COPY --from=ghcr.io/jdx/mise:2025.11.3@sha256:ac26f5978c0e2783f3e68e58ce75eddb83e41b89bf8747c503bac2aa9baf22c5 /usr/local/bin/mise /usr/local/bin/mise
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY ./plugins/mise.toml ./plugins/
|
COPY ./plugins/mise.toml ./plugins/
|
||||||
ENV MISE_TRUSTED_CONFIG_PATHS=/usr/src/app/plugins/mise.toml
|
ENV MISE_TRUSTED_CONFIG_PATHS=/usr/src/app/plugins/mise.toml
|
||||||
ENV MISE_DATA_DIR=/buildcache/mise
|
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
|
mise install --cd plugins
|
||||||
|
|
||||||
COPY ./plugins ./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=.pnpmfile.cjs,target=.pnpmfile.cjs \
|
||||||
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
|
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
|
||||||
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.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
|
cd plugins && mise run build
|
||||||
|
|
||||||
FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29
|
FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cron": "4.3.3",
|
"cron": "4.3.3",
|
||||||
"exiftool-vendored": "^33.0.0",
|
"exiftool-vendored": "^34.0.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"node-gyp": "^12.0.0",
|
"node-gyp": "^12.0.0",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"prettier": "^3.0.2",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"sql-formatter": "^15.0.0",
|
"sql-formatter": "^15.0.0",
|
||||||
"supertest": "^7.1.0",
|
"supertest": "^7.1.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { asOptions } from 'src/sql-tools/helpers';
|
|||||||
import { register } from 'src/sql-tools/register';
|
import { register } from 'src/sql-tools/register';
|
||||||
import { ColumnStorage, ColumnType, DatabaseEnum } from 'src/sql-tools/types';
|
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<unknown> | object | Date | (() => string);
|
||||||
|
|
||||||
export type ColumnBaseOptions = {
|
export type ColumnBaseOptions = {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ export const fromColumnValue = (columnValue?: ColumnValue) => {
|
|||||||
return `'${value.toISOString()}'`;
|
return `'${value.toISOString()}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return "'{}'";
|
||||||
|
}
|
||||||
|
|
||||||
return `'${String(value)}'`;
|
return `'${String(value)}'`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -394,6 +394,20 @@ describe(schemaDiff.name, () => {
|
|||||||
|
|
||||||
expect(diff.items).toEqual([]);
|
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([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
40
server/test/sql-tools/column-default-array.stub.ts
Normal file
40
server/test/sql-tools/column-default-array.stub.ts
Normal file
@@ -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: [],
|
||||||
|
};
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||||
"@immich/justified-layout-wasm": "^0.4.3",
|
"@immich/justified-layout-wasm": "^0.4.3",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@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",
|
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@photo-sphere-viewer/core": "^5.14.0",
|
"@photo-sphere-viewer/core": "^5.14.0",
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
"factory.ts": "^1.4.1",
|
"factory.ts": "^1.4.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"happy-dom": "^20.0.0",
|
"happy-dom": "^20.0.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"prettier-plugin-sort-json": "^4.1.1",
|
"prettier-plugin-sort-json": "^4.1.1",
|
||||||
"prettier-plugin-svelte": "^3.3.3",
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
|
|||||||
24
web/src/lib/components/HeaderActionButton.svelte
Normal file
24
web/src/lib/components/HeaderActionButton.svelte
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HeaderButtonActionItem } from '$lib/types';
|
||||||
|
import { Button } from '@immich/ui';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
action: HeaderButtonActionItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { action }: Props = $props();
|
||||||
|
const { title, icon, color = 'secondary', onAction } = $derived(action);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if action.$if?.() ?? true}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
{color}
|
||||||
|
leadingIcon={icon}
|
||||||
|
onclick={() => onAction(action)}
|
||||||
|
title={action.data?.title}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { type ActionItem, Button, Text } from '@immich/ui';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
action: ActionItem;
|
|
||||||
title?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { action, title: titleAttr }: Props = $props();
|
|
||||||
const { title, icon, color = 'secondary', onAction } = $derived(action);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if action.$if?.() ?? true}
|
|
||||||
<Button variant="ghost" size="small" {color} leadingIcon={icon} onclick={() => onAction(action)} title={titleAttr}>
|
|
||||||
<Text class="hidden md:block">{title}</Text>
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<tr
|
<tr
|
||||||
class="flex h-12 w-full place-items-center border-3 border-transparent p-2 text-center even:bg-subtle/20 odd:bg-subtle/80 hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5"
|
class="flex w-full place-items-center border-3 border-transparent p-2 text-center even:bg-subtle/20 odd:bg-subtle/80 hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:px-5 md:py-2"
|
||||||
onclick={() => goto(resolve(`${AppRoute.ALBUMS}/${album.id}`))}
|
onclick={() => goto(resolve(`${AppRoute.ALBUMS}/${album.id}`))}
|
||||||
{oncontextmenu}
|
{oncontextmenu}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -126,6 +126,7 @@
|
|||||||
|
|
||||||
const onMouseLeave = () => {
|
const onMouseLeave = () => {
|
||||||
mouseOver = false;
|
mouseOver = false;
|
||||||
|
onMouseEvent?.({ isMouseOver: false, selectedGroupIndex: groupIndex });
|
||||||
};
|
};
|
||||||
|
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PageContent from '$lib/components/layouts/PageContent.svelte';
|
import PageContent from '$lib/components/layouts/PageContent.svelte';
|
||||||
import TitleLayout from '$lib/components/layouts/TitleLayout.svelte';
|
|
||||||
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
||||||
import AdminSidebar from '$lib/sidebars/AdminSidebar.svelte';
|
import AdminSidebar from '$lib/sidebars/AdminSidebar.svelte';
|
||||||
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||||
import { AppShell, AppShellHeader, AppShellSidebar, Scrollable, type BreadcrumbItem } from '@immich/ui';
|
import type { HeaderButtonActionItem } from '$lib/types';
|
||||||
|
import {
|
||||||
|
AppShell,
|
||||||
|
AppShellHeader,
|
||||||
|
AppShellSidebar,
|
||||||
|
Breadcrumbs,
|
||||||
|
Button,
|
||||||
|
ContextMenuButton,
|
||||||
|
HStack,
|
||||||
|
MenuItemType,
|
||||||
|
Scrollable,
|
||||||
|
isMenuItemType,
|
||||||
|
type BreadcrumbItem,
|
||||||
|
} from '@immich/ui';
|
||||||
|
import { mdiSlashForward } from '@mdi/js';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
breadcrumbs: BreadcrumbItem[];
|
breadcrumbs: BreadcrumbItem[];
|
||||||
buttons?: Snippet;
|
actions?: Array<HeaderButtonActionItem | MenuItemType>;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { breadcrumbs, buttons, children }: Props = $props();
|
let { breadcrumbs, actions = [], children }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppShell>
|
<AppShell>
|
||||||
@@ -24,11 +38,37 @@
|
|||||||
<AdminSidebar />
|
<AdminSidebar />
|
||||||
</AppShellSidebar>
|
</AppShellSidebar>
|
||||||
|
|
||||||
<TitleLayout {breadcrumbs} {buttons}>
|
<div class="h-full flex flex-col">
|
||||||
|
<div class="flex h-16 w-full justify-between items-center border-b py-2 px-4 md:px-2">
|
||||||
|
<Breadcrumbs items={breadcrumbs} separator={mdiSlashForward} />
|
||||||
|
|
||||||
|
{#if actions.length > 0}
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<HStack gap={0}>
|
||||||
|
{#each actions as action, i (i)}
|
||||||
|
{#if !isMenuItemType(action) && (action.$if?.() ?? true)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
color={action.color ?? 'secondary'}
|
||||||
|
leadingIcon={action.icon}
|
||||||
|
onclick={() => action.onAction(action)}
|
||||||
|
title={action.data?.title}
|
||||||
|
>
|
||||||
|
{action.title}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</HStack>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ContextMenuButton aria-label={$t('open')} items={actions} class="md:hidden" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<Scrollable class="grow">
|
<Scrollable class="grow">
|
||||||
<PageContent>
|
<PageContent>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
</TitleLayout>
|
</div>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Breadcrumbs, type BreadcrumbItem } from '@immich/ui';
|
|
||||||
import { mdiSlashForward } from '@mdi/js';
|
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
breadcrumbs: BreadcrumbItem[];
|
|
||||||
buttons?: Snippet;
|
|
||||||
children?: Snippet;
|
|
||||||
};
|
|
||||||
|
|
||||||
let { breadcrumbs, buttons, children }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="h-full flex flex-col">
|
|
||||||
<div class="flex h-16 w-full place-items-center justify-between border-b p-2">
|
|
||||||
<Breadcrumbs items={breadcrumbs} separator={mdiSlashForward} />
|
|
||||||
{@render buttons?.()}
|
|
||||||
</div>
|
|
||||||
{@render children?.()}
|
|
||||||
</div>
|
|
||||||
@@ -28,7 +28,7 @@ export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResp
|
|||||||
title: $t('scan_all_libraries'),
|
title: $t('scan_all_libraries'),
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
icon: mdiSync,
|
icon: mdiSync,
|
||||||
onAction: () => void handleScanAllLibraries(),
|
onAction: () => handleScanAllLibraries(),
|
||||||
shortcuts: { shift: true, key: 'r' },
|
shortcuts: { shift: true, key: 'r' },
|
||||||
$if: () => libraries.length > 0,
|
$if: () => libraries.length > 0,
|
||||||
};
|
};
|
||||||
@@ -37,7 +37,7 @@ export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResp
|
|||||||
title: $t('create_library'),
|
title: $t('create_library'),
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
icon: mdiPlusBoxOutline,
|
icon: mdiPlusBoxOutline,
|
||||||
onAction: () => void handleCreateLibrary(),
|
onAction: () => handleCreateLibrary(),
|
||||||
shortcuts: { shift: true, key: 'n' },
|
shortcuts: { shift: true, key: 'n' },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
|
|||||||
icon: mdiPencilOutline,
|
icon: mdiPencilOutline,
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
title: $t('rename'),
|
title: $t('rename'),
|
||||||
onAction: () => void modalManager.show(LibraryRenameModal, { library }),
|
onAction: () => modalManager.show(LibraryRenameModal, { library }),
|
||||||
shortcuts: { key: 'r' },
|
shortcuts: { key: 'r' },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
|
|||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
title: $t('delete'),
|
title: $t('delete'),
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
onAction: () => void handleDeleteLibrary(library),
|
onAction: () => handleDeleteLibrary(library),
|
||||||
shortcuts: { key: 'Backspace' },
|
shortcuts: { key: 'Backspace' },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,21 +66,21 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
|
|||||||
icon: mdiPlusBoxOutline,
|
icon: mdiPlusBoxOutline,
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
title: $t('add'),
|
title: $t('add'),
|
||||||
onAction: () => void modalManager.show(LibraryFolderAddModal, { library }),
|
onAction: () => modalManager.show(LibraryFolderAddModal, { library }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddExclusionPattern: ActionItem = {
|
const AddExclusionPattern: ActionItem = {
|
||||||
icon: mdiPlusBoxOutline,
|
icon: mdiPlusBoxOutline,
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
title: $t('add'),
|
title: $t('add'),
|
||||||
onAction: () => void modalManager.show(LibraryExclusionPatternAddModal, { library }),
|
onAction: () => modalManager.show(LibraryExclusionPatternAddModal, { library }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Scan: ActionItem = {
|
const Scan: ActionItem = {
|
||||||
icon: mdiSync,
|
icon: mdiSync,
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
title: $t('scan_library'),
|
title: $t('scan_library'),
|
||||||
onAction: () => void handleScanLibrary(library),
|
onAction: () => handleScanLibrary(library),
|
||||||
shortcuts: { shift: true, key: 'r' },
|
shortcuts: { shift: true, key: 'r' },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,14 +92,14 @@ export const getLibraryFolderActions = ($t: MessageFormatter, library: LibraryRe
|
|||||||
icon: mdiPencilOutline,
|
icon: mdiPencilOutline,
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
title: $t('edit'),
|
title: $t('edit'),
|
||||||
onAction: () => void modalManager.show(LibraryFolderEditModal, { folder, library }),
|
onAction: () => modalManager.show(LibraryFolderEditModal, { folder, library }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Delete: ActionItem = {
|
const Delete: ActionItem = {
|
||||||
icon: mdiTrashCanOutline,
|
icon: mdiTrashCanOutline,
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
title: $t('delete'),
|
title: $t('delete'),
|
||||||
onAction: () => void handleDeleteLibraryFolder(library, folder),
|
onAction: () => handleDeleteLibraryFolder(library, folder),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { Edit, Delete };
|
return { Edit, Delete };
|
||||||
@@ -114,14 +114,14 @@ export const getLibraryExclusionPatternActions = (
|
|||||||
icon: mdiPencilOutline,
|
icon: mdiPencilOutline,
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
title: $t('edit'),
|
title: $t('edit'),
|
||||||
onAction: () => void modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }),
|
onAction: () => modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Delete: ActionItem = {
|
const Delete: ActionItem = {
|
||||||
icon: mdiTrashCanOutline,
|
icon: mdiTrashCanOutline,
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
title: $t('delete'),
|
title: $t('delete'),
|
||||||
onAction: () => void handleDeleteExclusionPattern(library, exclusionPattern),
|
onAction: () => handleDeleteExclusionPattern(library, exclusionPattern),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { Edit, Delete };
|
return { Edit, Delete };
|
||||||
@@ -273,7 +273,7 @@ const handleDeleteLibraryFolder = async (library: LibraryResponseDto, folder: st
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -285,10 +285,7 @@ const handleDeleteLibraryFolder = async (library: LibraryResponseDto, folder: st
|
|||||||
toastManager.success($t('admin.library_updated'));
|
toastManager.success($t('admin.library_updated'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.unable_to_update_library'));
|
handleError(error, $t('errors.unable_to_update_library'));
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleAddLibraryExclusionPattern = async (library: LibraryResponseDto, exclusionPattern: string) => {
|
export const handleAddLibraryExclusionPattern = async (library: LibraryResponseDto, exclusionPattern: string) => {
|
||||||
@@ -345,9 +342,8 @@ const handleDeleteExclusionPattern = async (library: LibraryResponseDto, exclusi
|
|||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
const confirmed = await modalManager.showDialog({ prompt: $t('admin.library_remove_exclusion_pattern_prompt') });
|
const confirmed = await modalManager.showDialog({ prompt: $t('admin.library_remove_exclusion_pattern_prompt') });
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -361,8 +357,5 @@ const handleDeleteExclusionPattern = async (library: LibraryResponseDto, exclusi
|
|||||||
toastManager.success($t('admin.library_updated'));
|
toastManager.success($t('admin.library_updated'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.unable_to_update_library'));
|
handleError(error, $t('errors.unable_to_update_library'));
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
|
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
||||||
import JobCreateModal from '$lib/modals/JobCreateModal.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 { handleError } from '$lib/utils/handle-error';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
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 { modalManager, toastManager, type ActionItem, type IconLike } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiClose,
|
mdiClose,
|
||||||
@@ -23,7 +32,6 @@ import {
|
|||||||
mdiPlay,
|
mdiPlay,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
mdiStateMachine,
|
mdiStateMachine,
|
||||||
mdiSync,
|
|
||||||
mdiTable,
|
mdiTable,
|
||||||
mdiTagFaces,
|
mdiTagFaces,
|
||||||
mdiTrashCanOutline,
|
mdiTrashCanOutline,
|
||||||
@@ -31,7 +39,6 @@ import {
|
|||||||
mdiVideo,
|
mdiVideo,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import type { MessageFormatter } from 'svelte-i18n';
|
import type { MessageFormatter } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
|
||||||
|
|
||||||
type QueueItem = {
|
type QueueItem = {
|
||||||
icon: IconLike;
|
icon: IconLike;
|
||||||
@@ -39,15 +46,17 @@ type QueueItem = {
|
|||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getQueuesActions = ($t: MessageFormatter) => {
|
export const getQueuesActions = ($t: MessageFormatter, queues: QueueResponseDto[] | undefined) => {
|
||||||
const ViewQueues: ActionItem = {
|
const pausedQueues = (queues ?? []).filter(({ isPaused }) => isPaused).map(({ name }) => name);
|
||||||
title: $t('admin.queues'),
|
|
||||||
description: $t('admin.queues_page_description'),
|
const ResumePaused: HeaderButtonActionItem = {
|
||||||
icon: mdiSync,
|
title: $t('resume_paused_jobs', { values: { count: pausedQueues.length } }),
|
||||||
type: $t('page'),
|
$if: () => pausedQueues.length > 0,
|
||||||
isGlobal: true,
|
icon: mdiPlay,
|
||||||
$if: () => get(user)?.isAdmin,
|
onAction: () => handleResumePausedJobs(pausedQueues),
|
||||||
onAction: () => goto(AppRoute.ADMIN_QUEUES),
|
data: {
|
||||||
|
title: pausedQueues.join(', '),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const CreateJob: ActionItem = {
|
const CreateJob: ActionItem = {
|
||||||
@@ -68,7 +77,7 @@ export const getQueuesActions = ($t: MessageFormatter) => {
|
|||||||
onAction: () => goto(`${AppRoute.ADMIN_SETTINGS}?isOpen=job`),
|
onAction: () => goto(`${AppRoute.ADMIN_SETTINGS}?isOpen=job`),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ViewQueues, ManageConcurrency, CreateJob };
|
return { ResumePaused, ManageConcurrency, CreateJob };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getQueueActions = ($t: MessageFormatter, queue: QueueResponseDto) => {
|
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 handleRemoveFailedJobs = async (queue: QueueResponseDto) => {
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
|||||||
@@ -24,26 +24,26 @@ export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLin
|
|||||||
const Edit: ActionItem = {
|
const Edit: ActionItem = {
|
||||||
title: $t('edit_link'),
|
title: $t('edit_link'),
|
||||||
icon: mdiPencilOutline,
|
icon: mdiPencilOutline,
|
||||||
onAction: () => void goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`),
|
onAction: () => goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Delete: ActionItem = {
|
const Delete: ActionItem = {
|
||||||
title: $t('delete_link'),
|
title: $t('delete_link'),
|
||||||
icon: mdiTrashCanOutline,
|
icon: mdiTrashCanOutline,
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
onAction: () => void handleDeleteSharedLink(sharedLink),
|
onAction: () => handleDeleteSharedLink(sharedLink),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Copy: ActionItem = {
|
const Copy: ActionItem = {
|
||||||
title: $t('copy_link'),
|
title: $t('copy_link'),
|
||||||
icon: mdiContentCopy,
|
icon: mdiContentCopy,
|
||||||
onAction: () => void copyToClipboard(asUrl(sharedLink)),
|
onAction: () => copyToClipboard(asUrl(sharedLink)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const ViewQrCode: ActionItem = {
|
const ViewQrCode: ActionItem = {
|
||||||
title: $t('view_qr_code'),
|
title: $t('view_qr_code'),
|
||||||
icon: mdiQrcode,
|
icon: mdiQrcode,
|
||||||
onAction: () => void handleShowSharedLinkQrCode(sharedLink),
|
onAction: () => handleShowSharedLinkQrCode(sharedLink),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { Edit, Delete, Copy, ViewQrCode };
|
return { Edit, Delete, Copy, ViewQrCode };
|
||||||
@@ -88,7 +88,7 @@ export const handleUpdateSharedLink = async (sharedLink: SharedLinkResponseDto,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto): Promise<boolean> => {
|
const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto) => {
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
const success = await modalManager.showDialog({
|
const success = await modalManager.showDialog({
|
||||||
title: $t('delete_shared_link'),
|
title: $t('delete_shared_link'),
|
||||||
@@ -96,17 +96,15 @@ export const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto):
|
|||||||
confirmText: $t('delete'),
|
confirmText: $t('delete'),
|
||||||
});
|
});
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await removeSharedLink({ id: sharedLink.id });
|
await removeSharedLink({ id: sharedLink.id });
|
||||||
eventManager.emit('SharedLinkDelete', sharedLink);
|
eventManager.emit('SharedLinkDelete', sharedLink);
|
||||||
toastManager.success($t('deleted_shared_link'));
|
toastManager.success($t('deleted_shared_link'));
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.unable_to_delete_shared_link'));
|
handleError(error, $t('errors.unable_to_delete_shared_link'));
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const getSystemConfigActions = (
|
|||||||
description: $t('admin.copy_config_to_clipboard_description'),
|
description: $t('admin.copy_config_to_clipboard_description'),
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
icon: mdiContentCopy,
|
icon: mdiContentCopy,
|
||||||
onAction: () => void handleCopyToClipboard(config),
|
onAction: () => handleCopyToClipboard(config),
|
||||||
shortcuts: { shift: true, key: 'c' },
|
shortcuts: { shift: true, key: 'c' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
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 PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte';
|
||||||
import UserCreateModal from '$lib/modals/UserCreateModal.svelte';
|
import UserCreateModal from '$lib/modals/UserCreateModal.svelte';
|
||||||
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
|
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
|
||||||
import UserEditModal from '$lib/modals/UserEditModal.svelte';
|
import UserEditModal from '$lib/modals/UserEditModal.svelte';
|
||||||
import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte';
|
import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte';
|
||||||
import { user as authUser } from '$lib/stores/user.store';
|
import { user as authUser } from '$lib/stores/user.store';
|
||||||
|
import type { HeaderButtonActionItem } from '$lib/types';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +30,7 @@ import {
|
|||||||
mdiPlusBoxOutline,
|
mdiPlusBoxOutline,
|
||||||
mdiTrashCanOutline,
|
mdiTrashCanOutline,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import type { MessageFormatter } from 'svelte-i18n';
|
import type { MessageFormatter } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ export const getUserAdminsActions = ($t: MessageFormatter) => {
|
|||||||
title: $t('create_user'),
|
title: $t('create_user'),
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
icon: mdiPlusBoxOutline,
|
icon: mdiPlusBoxOutline,
|
||||||
onAction: () => void modalManager.show(UserCreateModal, {}),
|
onAction: () => modalManager.show(UserCreateModal, {}),
|
||||||
shortcuts: { shift: true, key: 'n' },
|
shortcuts: { shift: true, key: 'n' },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,11 +63,17 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
|
|||||||
shortcuts: { key: 'Backspace' },
|
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,
|
icon: mdiDeleteRestore,
|
||||||
title: $t('restore'),
|
title: $t('restore'),
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
|
data: {
|
||||||
|
title: $t('admin.user_restore_scheduled_removal', { values: { date: getDeleteDate(user.deletedAt!) } }),
|
||||||
|
},
|
||||||
$if: () => !!user.deletedAt && user.status === UserStatus.Deleted,
|
$if: () => !!user.deletedAt && user.status === UserStatus.Deleted,
|
||||||
onAction: () => modalManager.show(UserRestoreConfirmModal, { user }),
|
onAction: () => modalManager.show(UserRestoreConfirmModal, { user }),
|
||||||
};
|
};
|
||||||
@@ -74,14 +83,14 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
|
|||||||
title: $t('reset_password'),
|
title: $t('reset_password'),
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
$if: () => get(authUser).id !== user.id,
|
$if: () => get(authUser).id !== user.id,
|
||||||
onAction: () => void handleResetPasswordUserAdmin(user),
|
onAction: () => handleResetPasswordUserAdmin(user),
|
||||||
};
|
};
|
||||||
|
|
||||||
const ResetPinCode: ActionItem = {
|
const ResetPinCode: ActionItem = {
|
||||||
icon: mdiLockSmart,
|
icon: mdiLockSmart,
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
title: $t('reset_pin_code'),
|
title: $t('reset_pin_code'),
|
||||||
onAction: () => void handleResetPinCodeUserAdmin(user),
|
onAction: () => handleResetPinCodeUserAdmin(user),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { Update, Delete, Restore, ResetPassword, ResetPinCode };
|
return { Update, Delete, Restore, ResetPassword, ResetPinCode };
|
||||||
@@ -162,12 +171,12 @@ const generatePassword = (length: number = 16) => {
|
|||||||
return generatedPassword;
|
return generatedPassword;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleResetPasswordUserAdmin = async (user: UserAdminResponseDto) => {
|
const handleResetPasswordUserAdmin = async (user: UserAdminResponseDto) => {
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
const prompt = $t('admin.confirm_user_password_reset', { values: { user: user.name } });
|
const prompt = $t('admin.confirm_user_password_reset', { values: { user: user.name } });
|
||||||
const success = await modalManager.showDialog({ prompt });
|
const success = await modalManager.showDialog({ prompt });
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -176,28 +185,24 @@ export const handleResetPasswordUserAdmin = async (user: UserAdminResponseDto) =
|
|||||||
eventManager.emit('UserAdminUpdate', response);
|
eventManager.emit('UserAdminUpdate', response);
|
||||||
toastManager.success();
|
toastManager.success();
|
||||||
await modalManager.show(PasswordResetSuccessModal, { newPassword: dto.password });
|
await modalManager.show(PasswordResetSuccessModal, { newPassword: dto.password });
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.unable_to_reset_password'));
|
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 $t = await getFormatter();
|
||||||
const prompt = $t('admin.confirm_user_pin_code_reset', { values: { user: user.name } });
|
const prompt = $t('admin.confirm_user_pin_code_reset', { values: { user: user.name } });
|
||||||
const success = await modalManager.showDialog({ prompt });
|
const success = await modalManager.showDialog({ prompt });
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await updateUserAdmin({ id: user.id, userAdminUpdateDto: { pinCode: null } });
|
const response = await updateUserAdmin({ id: user.id, userAdminUpdateDto: { pinCode: null } });
|
||||||
eventManager.emit('UserAdminUpdate', response);
|
eventManager.emit('UserAdminUpdate', response);
|
||||||
toastManager.success($t('pin_code_reset_successfully'));
|
toastManager.success($t('pin_code_reset_successfully'));
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.unable_to_reset_pin_code'));
|
handleError(error, $t('errors.unable_to_reset_pin_code'));
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk';
|
import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk';
|
||||||
|
import type { ActionItem } from '@immich/ui';
|
||||||
|
|
||||||
export interface ReleaseEvent {
|
export interface ReleaseEvent {
|
||||||
isAvailable: boolean;
|
isAvailable: boolean;
|
||||||
@@ -9,3 +10,5 @@ export interface ReleaseEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type QueueSnapshot = { timestamp: number; snapshot?: QueueResponseDto[] };
|
export type QueueSnapshot = { timestamp: number; snapshot?: QueueResponseDto[] };
|
||||||
|
|
||||||
|
export type HeaderButtonActionItem = ActionItem & { data?: { title?: string } };
|
||||||
|
|||||||
@@ -14,15 +14,15 @@
|
|||||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||||
import ServerRestartingModal from '$lib/modals/ServerRestartingModal.svelte';
|
import ServerRestartingModal from '$lib/modals/ServerRestartingModal.svelte';
|
||||||
import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.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 { user } from '$lib/stores/user.store';
|
||||||
import { closeWebsocketConnection, openWebsocketConnection, websocketStore } from '$lib/stores/websocket';
|
import { closeWebsocketConnection, openWebsocketConnection, websocketStore } from '$lib/stores/websocket';
|
||||||
import type { ReleaseEvent } from '$lib/types';
|
import type { ReleaseEvent } from '$lib/types';
|
||||||
import { copyToClipboard, getReleaseType, semverToName } from '$lib/utils';
|
import { copyToClipboard, getReleaseType, semverToName } from '$lib/utils';
|
||||||
import { maintenanceShouldRedirect } from '$lib/utils/maintenance';
|
import { maintenanceShouldRedirect } from '$lib/utils/maintenance';
|
||||||
import { isAssetViewerRoute } from '$lib/utils/navigation';
|
import { isAssetViewerRoute } from '$lib/utils/navigation';
|
||||||
import { CommandPaletteContext, modalManager, setTranslations, type ActionItem } from '@immich/ui';
|
import { CommandPaletteContext, modalManager, setTranslations, toastManager, type ActionItem } from '@immich/ui';
|
||||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiThemeLightDark } from '@mdi/js';
|
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiThemeLightDark } from '@mdi/js';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
@@ -53,6 +53,8 @@
|
|||||||
return new URL(page.url.pathname + page.url.search, 'https://my.immich.app');
|
return new URL(page.url.pathname + page.url.search, 'https://my.immich.app');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toastManager.setOptions({ class: 'top-16' });
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const element = document.querySelector('#stencil');
|
const element = document.querySelector('#stencil');
|
||||||
element?.remove();
|
element?.remove();
|
||||||
@@ -62,6 +64,10 @@
|
|||||||
eventManager.emit('AppInit');
|
eventManager.emit('AppInit');
|
||||||
|
|
||||||
beforeNavigate(({ from, to }) => {
|
beforeNavigate(({ from, to }) => {
|
||||||
|
if (sidebarStore.isOpen) {
|
||||||
|
sidebarStore.reset();
|
||||||
|
}
|
||||||
|
|
||||||
if (isAssetViewerRoute(from) && isAssetViewerRoute(to)) {
|
if (isAssetViewerRoute(from) && isAssetViewerRoute(to)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -149,6 +155,13 @@
|
|||||||
icon: mdiCog,
|
icon: mdiCog,
|
||||||
onAction: () => goto(AppRoute.ADMIN_SETTINGS),
|
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'),
|
title: $t('external_libraries'),
|
||||||
description: $t('admin.external_libraries_page_description'),
|
description: $t('admin.external_libraries_page_description'),
|
||||||
@@ -163,7 +176,7 @@
|
|||||||
},
|
},
|
||||||
].map((route) => ({ ...route, type: $t('page'), isGlobal: true, $if: () => $user?.isAdmin }));
|
].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]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<OnEvents {onReleaseEvent} />
|
<OnEvents {onReleaseEvent} />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
|
||||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||||
@@ -60,17 +59,11 @@
|
|||||||
|
|
||||||
<CommandPaletteContext commands={[Create, ScanAll]} />
|
<CommandPaletteContext commands={[Create, ScanAll]} />
|
||||||
|
|
||||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[ScanAll, Create]}>
|
||||||
{#snippet buttons()}
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<HeaderButton action={ScanAll} />
|
|
||||||
<HeaderButton action={Create} />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
<section class="my-4">
|
<section class="my-4">
|
||||||
<div class="flex flex-col items-center gap-2" in:fade={{ duration: 500 }}>
|
<div class="flex flex-col items-center gap-2" in:fade={{ duration: 500 }}>
|
||||||
{#if libraries.length > 0}
|
{#if libraries.length > 0}
|
||||||
<table class="w-3/4 text-start">
|
<table class="text-start">
|
||||||
<thead
|
<thead
|
||||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const load = (async ({ url }) => {
|
|||||||
statistics: Object.fromEntries(statistics),
|
statistics: Object.fromEntries(statistics),
|
||||||
owners: Object.fromEntries(owners),
|
owners: Object.fromEntries(owners),
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('admin.external_library_management'),
|
title: $t('external_libraries'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}) satisfies PageLoad;
|
}) satisfies PageLoad;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import emptyFoldersUrl from '$lib/assets/empty-folders.svg';
|
import emptyFoldersUrl from '$lib/assets/empty-folders.svg';
|
||||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
import HeaderActionButton from '$lib/components/HeaderActionButton.svelte';
|
||||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
import ServerStatisticsCard from '$lib/components/server-statistics/ServerStatisticsCard.svelte';
|
import ServerStatisticsCard from '$lib/components/server-statistics/ServerStatisticsCard.svelte';
|
||||||
@@ -53,18 +53,9 @@
|
|||||||
<CommandPaletteContext commands={[Rename, Delete, AddFolder, AddExclusionPattern, Scan]} />
|
<CommandPaletteContext commands={[Rename, Delete, AddFolder, AddExclusionPattern, Scan]} />
|
||||||
|
|
||||||
<AdminPageLayout
|
<AdminPageLayout
|
||||||
breadcrumbs={[
|
breadcrumbs={[{ title: $t('external_libraries'), href: AppRoute.ADMIN_LIBRARY_MANAGEMENT }, { title: library.name }]}
|
||||||
{ title: $t('admin.external_library_management'), href: AppRoute.ADMIN_LIBRARY_MANAGEMENT },
|
actions={[Scan, Rename, Delete]}
|
||||||
{ title: library.name },
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
{#snippet buttons()}
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<HeaderButton action={Scan} />
|
|
||||||
<HeaderButton action={Rename} />
|
|
||||||
<HeaderButton action={Delete} />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
<Container size="large" center>
|
<Container size="large" center>
|
||||||
<div class="grid gap-4 grid-cols-1 lg:grid-cols-2 w-full">
|
<div class="grid gap-4 grid-cols-1 lg:grid-cols-2 w-full">
|
||||||
<Heading tag="h1" size="large" class="col-span-full my-4">{library.name}</Heading>
|
<Heading tag="h1" size="large" class="col-span-full my-4">{library.name}</Heading>
|
||||||
@@ -80,7 +71,7 @@
|
|||||||
<Icon icon={mdiFolderOutline} size="1.5rem" />
|
<Icon icon={mdiFolderOutline} size="1.5rem" />
|
||||||
<CardTitle>{$t('folders')}</CardTitle>
|
<CardTitle>{$t('folders')}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<HeaderButton action={AddFolder} />
|
<HeaderActionButton action={AddFolder} />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
@@ -120,7 +111,7 @@
|
|||||||
<Icon icon={mdiFilterMinusOutline} size="1.5rem" />
|
<Icon icon={mdiFilterMinusOutline} size="1.5rem" />
|
||||||
<CardTitle>{$t('exclusion_pattern')}</CardTitle>
|
<CardTitle>{$t('exclusion_pattern')}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<HeaderButton action={AddExclusionPattern} />
|
<HeaderActionButton action={AddExclusionPattern} />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
|
||||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
import JobsPanel from '$lib/components/QueuePanel.svelte';
|
import JobsPanel from '$lib/components/QueuePanel.svelte';
|
||||||
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
||||||
import { getQueuesActions } from '$lib/services/queue.service';
|
import { getQueuesActions } from '$lib/services/queue.service';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { type QueueResponseDto } from '@immich/sdk';
|
||||||
import { QueueCommand, runQueueCommandLegacy, type QueueResponseDto } from '@immich/sdk';
|
import { CommandPaletteContext, type ActionItem } from '@immich/ui';
|
||||||
import { Button, CommandPaletteContext, HStack, Text, type ActionItem } from '@immich/ui';
|
|
||||||
import { mdiPlay } from '@mdi/js';
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
@@ -22,20 +19,8 @@
|
|||||||
onMount(() => queueManager.listen());
|
onMount(() => queueManager.listen());
|
||||||
|
|
||||||
let queues = $derived<QueueResponseDto[]>(queueManager.queues);
|
let queues = $derived<QueueResponseDto[]>(queueManager.queues);
|
||||||
const pausedQueues = $derived(queues.filter(({ isPaused }) => isPaused).map(({ name }) => name));
|
|
||||||
|
|
||||||
const handleResumePausedJobs = async () => {
|
const { ResumePaused, CreateJob, ManageConcurrency } = $derived(getQueuesActions($t, queueManager.queues));
|
||||||
try {
|
|
||||||
for (const name of pausedQueues) {
|
|
||||||
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 { CreateJob, ManageConcurrency } = $derived(getQueuesActions($t));
|
|
||||||
const commands: ActionItem[] = $derived([CreateJob, ManageConcurrency]);
|
const commands: ActionItem[] = $derived([CreateJob, ManageConcurrency]);
|
||||||
|
|
||||||
const onQueueUpdate = (update: QueueResponseDto) => {
|
const onQueueUpdate = (update: QueueResponseDto) => {
|
||||||
@@ -52,27 +37,7 @@
|
|||||||
|
|
||||||
<OnEvents {onQueueUpdate} />
|
<OnEvents {onQueueUpdate} />
|
||||||
|
|
||||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[ResumePaused, CreateJob, ManageConcurrency]}>
|
||||||
{#snippet buttons()}
|
|
||||||
<HStack gap={0}>
|
|
||||||
{#if pausedQueues.length > 0}
|
|
||||||
<Button
|
|
||||||
leadingIcon={mdiPlay}
|
|
||||||
onclick={handleResumePausedJobs}
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
title={pausedQueues.join(', ')}
|
|
||||||
>
|
|
||||||
<Text class="hidden md:block">
|
|
||||||
{$t('resume_paused_jobs', { values: { count: pausedQueues.length } })}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
<HeaderButton action={CreateJob} />
|
|
||||||
<HeaderButton action={ManageConcurrency} />
|
|
||||||
</HStack>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
||||||
<section class="w-full pb-28 sm:w-5/6 md:w-212.5">
|
<section class="w-full pb-28 sm:w-5/6 md:w-212.5">
|
||||||
{#if queues}
|
{#if queues}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
|
||||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
import QueueGraph from '$lib/components/QueueGraph.svelte';
|
import QueueGraph from '$lib/components/QueueGraph.svelte';
|
||||||
@@ -7,7 +6,18 @@
|
|||||||
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
||||||
import { asQueueItem, getQueueActions } from '$lib/services/queue.service';
|
import { asQueueItem, getQueueActions } from '$lib/services/queue.service';
|
||||||
import { type QueueResponseDto } from '@immich/sdk';
|
import { type QueueResponseDto } from '@immich/sdk';
|
||||||
import { Badge, Card, CardBody, CardHeader, CardTitle, Container, Heading, HStack, Icon, Text } from '@immich/ui';
|
import {
|
||||||
|
Badge,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
Icon,
|
||||||
|
MenuItemType,
|
||||||
|
Text,
|
||||||
|
} from '@immich/ui';
|
||||||
import { mdiClockTimeTwoOutline } from '@mdi/js';
|
import { mdiClockTimeTwoOutline } from '@mdi/js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@@ -35,15 +45,10 @@
|
|||||||
|
|
||||||
<OnEvents {onQueueUpdate} />
|
<OnEvents {onQueueUpdate} />
|
||||||
|
|
||||||
<AdminPageLayout breadcrumbs={[{ title: $t('admin.queues'), href: AppRoute.ADMIN_QUEUES }, { title: item.title }]}>
|
<AdminPageLayout
|
||||||
{#snippet buttons()}
|
breadcrumbs={[{ title: $t('admin.queues'), href: AppRoute.ADMIN_QUEUES }, { title: item.title }]}
|
||||||
<HStack gap={0}>
|
actions={[Pause, Resume, Empty, MenuItemType.Divider, RemoveFailedJobs]}
|
||||||
<HeaderButton action={Pause} />
|
>
|
||||||
<HeaderButton action={Resume} />
|
|
||||||
<HeaderButton action={Empty} />
|
|
||||||
<HeaderButton action={RemoveFailedJobs} />
|
|
||||||
</HStack>
|
|
||||||
{/snippet}
|
|
||||||
<div>
|
<div>
|
||||||
<Container size="large" center>
|
<Container size="large" center>
|
||||||
<div class="mb-1 mt-4 flex items-center gap-2">
|
<div class="mb-1 mt-4 flex items-center gap-2">
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
import ThemeSettings from '$lib/components/admin-settings/ThemeSettings.svelte';
|
import ThemeSettings from '$lib/components/admin-settings/ThemeSettings.svelte';
|
||||||
import TrashSettings from '$lib/components/admin-settings/TrashSettings.svelte';
|
import TrashSettings from '$lib/components/admin-settings/TrashSettings.svelte';
|
||||||
import UserSettings from '$lib/components/admin-settings/UserSettings.svelte';
|
import UserSettings from '$lib/components/admin-settings/UserSettings.svelte';
|
||||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
|
||||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||||
import SettingAccordionState from '$lib/components/shared-components/settings/setting-accordion-state.svelte';
|
import SettingAccordionState from '$lib/components/shared-components/settings/setting-accordion-state.svelte';
|
||||||
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
||||||
@@ -27,7 +26,7 @@
|
|||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||||
import { getSystemConfigActions } from '$lib/services/system-config.service';
|
import { getSystemConfigActions } from '$lib/services/system-config.service';
|
||||||
import { Alert, CommandPaletteContext, HStack } from '@immich/ui';
|
import { Alert, CommandPaletteContext } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiAccountOutline,
|
mdiAccountOutline,
|
||||||
mdiBackupRestore,
|
mdiBackupRestore,
|
||||||
@@ -217,24 +216,13 @@
|
|||||||
|
|
||||||
<CommandPaletteContext commands={[CopyToClipboard, Upload, Download]} />
|
<CommandPaletteContext commands={[CopyToClipboard, Upload, Download]} />
|
||||||
|
|
||||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[CopyToClipboard, Download, Upload]}>
|
||||||
{#snippet buttons()}
|
<section id="setting-content" class="flex place-content-center sm:mx-4 mt-4">
|
||||||
<HStack gap={1}>
|
|
||||||
<div class="hidden lg:block">
|
|
||||||
<SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
|
|
||||||
</div>
|
|
||||||
<HeaderButton action={CopyToClipboard} />
|
|
||||||
<HeaderButton action={Download} />
|
|
||||||
<HeaderButton action={Upload} />
|
|
||||||
</HStack>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
|
||||||
<section class="w-full pb-28 sm:w-5/6 md:w-4xl">
|
<section class="w-full pb-28 sm:w-5/6 md:w-4xl">
|
||||||
{#if featureFlagsManager.value.configFile}
|
{#if featureFlagsManager.value.configFile}
|
||||||
<Alert color="warning" class="text-dark my-4" title={$t('admin.config_set_by_file')} />
|
<Alert color="warning" class="text-dark my-4" title={$t('admin.config_set_by_file')} />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="block lg:hidden">
|
<div>
|
||||||
<SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
|
<SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
|
||||||
</div>
|
</div>
|
||||||
<SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
|
<SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
|
||||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
import { getUserAdminsActions, handleNavigateUserAdmin } from '$lib/services/user-admin.service';
|
import { getUserAdminsActions, handleNavigateUserAdmin } from '$lib/services/user-admin.service';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||||
import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||||
import { Button, CommandPaletteContext, HStack, Icon } from '@immich/ui';
|
import { Button, CommandPaletteContext, Icon } from '@immich/ui';
|
||||||
import { mdiInfinity } from '@mdi/js';
|
import { mdiInfinity } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
@@ -45,12 +44,7 @@
|
|||||||
|
|
||||||
<CommandPaletteContext commands={[Create]} />
|
<CommandPaletteContext commands={[Create]} />
|
||||||
|
|
||||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[Create]}>
|
||||||
{#snippet buttons()}
|
|
||||||
<HStack gap={1}>
|
|
||||||
<HeaderButton action={Create} />
|
|
||||||
</HStack>
|
|
||||||
{/snippet}
|
|
||||||
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
||||||
<section class="w-full pb-28 lg:w-212.5">
|
<section class="w-full pb-28 lg:w-212.5">
|
||||||
<table class="my-5 w-full text-start">
|
<table class="my-5 w-full text-start">
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
|
||||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
import ServerStatisticsCard from '$lib/components/server-statistics/ServerStatisticsCard.svelte';
|
import ServerStatisticsCard from '$lib/components/server-statistics/ServerStatisticsCard.svelte';
|
||||||
@@ -8,7 +7,6 @@
|
|||||||
import DeviceCard from '$lib/components/user-settings-page/device-card.svelte';
|
import DeviceCard from '$lib/components/user-settings-page/device-card.svelte';
|
||||||
import FeatureSetting from '$lib/components/users/FeatureSetting.svelte';
|
import FeatureSetting from '$lib/components/users/FeatureSetting.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
|
||||||
import { getUserAdminActions } from '$lib/services/user-admin.service';
|
import { getUserAdminActions } from '$lib/services/user-admin.service';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { createDateFormatter, findLocale } from '$lib/utils';
|
import { createDateFormatter, findLocale } from '$lib/utils';
|
||||||
@@ -26,8 +24,8 @@
|
|||||||
Container,
|
Container,
|
||||||
getByteUnitString,
|
getByteUnitString,
|
||||||
Heading,
|
Heading,
|
||||||
HStack,
|
|
||||||
Icon,
|
Icon,
|
||||||
|
MenuItemType,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
} from '@immich/ui';
|
} from '@immich/ui';
|
||||||
@@ -42,15 +40,14 @@
|
|||||||
mdiPlayCircle,
|
mdiPlayCircle,
|
||||||
mdiTrashCanOutline,
|
mdiTrashCanOutline,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
}
|
};
|
||||||
|
|
||||||
let { data }: Props = $props();
|
const { data }: Props = $props();
|
||||||
|
|
||||||
let user = $derived(data.user);
|
let user = $derived(data.user);
|
||||||
const userPreferences = $derived(data.userPreferences);
|
const userPreferences = $derived(data.userPreferences);
|
||||||
@@ -94,9 +91,6 @@
|
|||||||
await goto(AppRoute.ADMIN_USERS);
|
await goto(AppRoute.ADMIN_USERS);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDeleteDate = (deletedAt: string): Date =>
|
|
||||||
DateTime.fromISO(deletedAt).plus({ days: serverConfigManager.value.userDeleteDelay }).toJSDate();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<OnEvents
|
<OnEvents
|
||||||
@@ -110,19 +104,8 @@
|
|||||||
|
|
||||||
<AdminPageLayout
|
<AdminPageLayout
|
||||||
breadcrumbs={[{ title: $t('admin.user_management'), href: AppRoute.ADMIN_USERS }, { title: user.name }]}
|
breadcrumbs={[{ title: $t('admin.user_management'), href: AppRoute.ADMIN_USERS }, { title: user.name }]}
|
||||||
|
actions={[ResetPassword, ResetPinCode, Update, Restore, MenuItemType.Divider, Delete]}
|
||||||
>
|
>
|
||||||
{#snippet buttons()}
|
|
||||||
<HStack gap={0}>
|
|
||||||
<HeaderButton action={ResetPassword} />
|
|
||||||
<HeaderButton action={ResetPinCode} />
|
|
||||||
<HeaderButton action={Update} />
|
|
||||||
<HeaderButton
|
|
||||||
action={Restore}
|
|
||||||
title={$t('admin.user_restore_scheduled_removal', { values: { date: getDeleteDate(user.deletedAt!) } })}
|
|
||||||
/>
|
|
||||||
<HeaderButton action={Delete} />
|
|
||||||
</HStack>
|
|
||||||
{/snippet}
|
|
||||||
<div>
|
<div>
|
||||||
<Container size="large" center>
|
<Container size="large" center>
|
||||||
{#if user.deletedAt}
|
{#if user.deletedAt}
|
||||||
|
|||||||
Reference in New Issue
Block a user