mirror of
https://github.com/immich-app/immich.git
synced 2025-12-11 01:10:23 +03:00
Compare commits
12 Commits
push-lrzks
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff8d5a341b | ||
|
|
7af99b8606 | ||
|
|
01e39277e0 | ||
|
|
06e79703da | ||
|
|
c360781565 | ||
|
|
287f6d5c94 | ||
|
|
fe9125a3d1 | ||
|
|
8b31936bb6 | ||
|
|
19958dfd83 | ||
|
|
1e1cf0d1fe | ||
|
|
879e0ea131 | ||
|
|
42136f9091 |
8
.github/workflows/build-mobile.yml
vendored
8
.github/workflows/build-mobile.yml
vendored
@@ -222,6 +222,7 @@ jobs:
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
bundler-cache: true
|
||||
working-directory: ./mobile/ios
|
||||
|
||||
- name: Install CocoaPods dependencies
|
||||
@@ -229,13 +230,6 @@ jobs:
|
||||
run: |
|
||||
pod install
|
||||
|
||||
- name: Install Fastlane
|
||||
working-directory: ./mobile/ios
|
||||
run: |
|
||||
gem install bundler
|
||||
bundle config set --local path 'vendor/bundle'
|
||||
bundle install
|
||||
|
||||
- name: Create API Key
|
||||
env:
|
||||
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"exiftool-vendored": "^33.0.0",
|
||||
"exiftool-vendored": "^34.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"jose": "^5.6.3",
|
||||
"luxon": "^3.4.4",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"backup_options_page_title": "Backup options",
|
||||
"backup_setting_subtitle": "Manage background and foreground upload settings",
|
||||
"backup_settings_subtitle": "Manage upload settings",
|
||||
"backup_upload_details_page_more_details": "Tap for more details",
|
||||
"backward": "Backward",
|
||||
"biometric_auth_enabled": "Biometric authentication enabled",
|
||||
"biometric_locked_out": "You are locked out of biometric authentication",
|
||||
@@ -718,6 +719,7 @@
|
||||
"check_corrupt_asset_backup_button": "Perform check",
|
||||
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
|
||||
"check_logs": "Check Logs",
|
||||
"checksum": "Checksum",
|
||||
"choose_matching_people_to_merge": "Choose matching people to merge",
|
||||
"city": "City",
|
||||
"clear": "Clear",
|
||||
@@ -1166,6 +1168,7 @@
|
||||
"header_settings_header_name_input": "Header name",
|
||||
"header_settings_header_value_input": "Header value",
|
||||
"headers_settings_tile_title": "Custom proxy headers",
|
||||
"height": "Height",
|
||||
"hi_user": "Hi {name} ({email})",
|
||||
"hide_all_people": "Hide all people",
|
||||
"hide_gallery": "Hide gallery",
|
||||
@@ -1288,6 +1291,7 @@
|
||||
"local": "Local",
|
||||
"local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server",
|
||||
"local_assets": "Local Assets",
|
||||
"local_id": "Local ID",
|
||||
"local_media_summary": "Local Media Summary",
|
||||
"local_network": "Local network",
|
||||
"local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
|
||||
@@ -2218,6 +2222,7 @@
|
||||
"week": "Week",
|
||||
"welcome": "Welcome",
|
||||
"welcome_to_immich": "Welcome to Immich",
|
||||
"width": "Width",
|
||||
"wifi_name": "Wi-Fi Name",
|
||||
"workflow": "Workflow",
|
||||
"wrong_pin_code": "Wrong PIN code",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"flutter": "3.35.7"
|
||||
"flutter": "3.38.4"
|
||||
}
|
||||
@@ -7,7 +7,7 @@ environment:
|
||||
dependencies:
|
||||
analyzer: ^7.0.0
|
||||
analyzer_plugin: ^0.13.0
|
||||
custom_lint_builder: ^0.7.5
|
||||
custom_lint_builder: 0.8.1
|
||||
glob: ^2.1.2
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
@@ -98,7 +98,7 @@ class DriftUploadDetailPage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Tap for more details',
|
||||
"backup_upload_details_page_more_details".t(context: context),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
@@ -239,14 +239,20 @@ class FileDetailDialog extends ConsumerWidget {
|
||||
const SizedBox(height: 24),
|
||||
if (asset != null) ...[
|
||||
_buildInfoSection(context, [
|
||||
_buildInfoRow(context, "Filename", path.basename(uploadStatus.filename)),
|
||||
_buildInfoRow(context, "Local ID", asset.id),
|
||||
_buildInfoRow(context, "File Size", formatHumanReadableBytes(uploadStatus.fileSize, 2)),
|
||||
if (asset.width != null) _buildInfoRow(context, "Width", "${asset.width}px"),
|
||||
if (asset.height != null) _buildInfoRow(context, "Height", "${asset.height}px"),
|
||||
_buildInfoRow(context, "Created At", asset.createdAt.toString()),
|
||||
_buildInfoRow(context, "Updated At", asset.updatedAt.toString()),
|
||||
if (asset.checksum != null) _buildInfoRow(context, "Checksum", asset.checksum!),
|
||||
_buildInfoRow(context, "filename".t(context: context), path.basename(uploadStatus.filename)),
|
||||
_buildInfoRow(context, "local_id".t(context: context), asset.id),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"file_size".t(context: context),
|
||||
formatHumanReadableBytes(uploadStatus.fileSize, 2),
|
||||
),
|
||||
if (asset.width != null) _buildInfoRow(context, "width".t(context: context), "${asset.width}px"),
|
||||
if (asset.height != null)
|
||||
_buildInfoRow(context, "height".t(context: context), "${asset.height}px"),
|
||||
_buildInfoRow(context, "created_at".t(context: context), asset.createdAt.toString()),
|
||||
_buildInfoRow(context, "updated_at".t(context: context), asset.updatedAt.toString()),
|
||||
if (asset.checksum != null)
|
||||
_buildInfoRow(context, "checksum".t(context: context), asset.checksum!),
|
||||
]),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -27,8 +27,19 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
||||
bool isAlbumTitleTextFieldFocus = false;
|
||||
Set<BaseAsset> selectedAssets = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
albumTitleController.addListener(_onTitleChanged);
|
||||
}
|
||||
|
||||
void _onTitleChanged() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
albumTitleController.removeListener(_onTitleChanged);
|
||||
albumTitleController.dispose();
|
||||
albumDescriptionController.dispose();
|
||||
albumTitleTextFieldFocusNode.dispose();
|
||||
|
||||
@@ -22,7 +22,9 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee
|
||||
enum AddToMenuItem { album, archive, unarchive, lockedFolder }
|
||||
|
||||
class AddActionButton extends ConsumerStatefulWidget {
|
||||
const AddActionButton({super.key});
|
||||
const AddActionButton({super.key, this.originalTheme});
|
||||
|
||||
final ThemeData? originalTheme;
|
||||
|
||||
@override
|
||||
ConsumerState<AddActionButton> createState() => _AddActionButtonState();
|
||||
@@ -71,7 +73,7 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
),
|
||||
|
||||
if (isOwner) ...[
|
||||
const PopupMenuDivider(),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text("move_to".tr(), style: context.textTheme.labelMedium),
|
||||
@@ -166,16 +168,27 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final themeData = widget.originalTheme ?? context.themeData;
|
||||
|
||||
return MenuAnchor(
|
||||
consumeOutsideTap: true,
|
||||
style: MenuStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor),
|
||||
backgroundColor: WidgetStatePropertyAll(themeData.scaffoldBackgroundColor),
|
||||
surfaceTintColor: const WidgetStatePropertyAll(Colors.grey),
|
||||
elevation: const WidgetStatePropertyAll(4),
|
||||
shape: const WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
|
||||
),
|
||||
menuChildren: _buildMenuChildren(),
|
||||
menuChildren: widget.originalTheme != null
|
||||
? [
|
||||
Theme(
|
||||
data: widget.originalTheme!,
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: _buildMenuChildren()),
|
||||
),
|
||||
]
|
||||
: _buildMenuChildren(),
|
||||
builder: (context, controller, child) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.add,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class BaseActionButton extends StatelessWidget {
|
||||
class BaseActionButton extends ConsumerWidget {
|
||||
const BaseActionButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
@@ -30,7 +31,7 @@ class BaseActionButton extends StatelessWidget {
|
||||
final void Function()? onLongPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final miniWidth = minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
|
||||
final iconTheme = IconTheme.of(context);
|
||||
final iconSize = iconTheme.size ?? 24.0;
|
||||
@@ -46,14 +47,13 @@ class BaseActionButton extends StatelessWidget {
|
||||
|
||||
if (menuItem) {
|
||||
final theme = context.themeData;
|
||||
final effectiveStyle = theme.textTheme.labelLarge;
|
||||
final effectiveIconColor = iconColor ?? theme.iconTheme.color ?? theme.colorScheme.onSurfaceVariant;
|
||||
|
||||
return MenuItemButton(
|
||||
style: MenuItemButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12)),
|
||||
leadingIcon: Icon(iconData, color: effectiveIconColor, size: 20),
|
||||
style: MenuItemButton.styleFrom(alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16)),
|
||||
leadingIcon: Icon(iconData, color: effectiveIconColor),
|
||||
onPressed: onPressed,
|
||||
child: Text(label, style: effectiveStyle),
|
||||
child: Text(label, style: theme.textTheme.labelLarge?.copyWith(fontSize: 16)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
||||
|
||||
class CastActionButton extends ConsumerWidget {
|
||||
const CastActionButton({super.key, this.iconOnly = true, this.menuItem = false});
|
||||
const CastActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu
|
||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||
|
||||
class MotionPhotoActionButton extends ConsumerWidget {
|
||||
const MotionPhotoActionButton({super.key, this.iconOnly = true, this.menuItem = false});
|
||||
const MotionPhotoActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
@@ -38,11 +38,13 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
opacity = 0;
|
||||
}
|
||||
|
||||
final originalTheme = context.themeData;
|
||||
|
||||
final actions = <Widget>[
|
||||
const ShareActionButton(source: ActionSource.viewer),
|
||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||
if (asset.type == AssetType.image) const EditImageActionButton(),
|
||||
if (asset.hasRemote) const AddActionButton(),
|
||||
if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
|
||||
|
||||
if (isOwner) ...[
|
||||
asset.isLocalOnly
|
||||
|
||||
@@ -4,26 +4,19 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
const ViewerTopAppBar({super.key});
|
||||
@@ -42,15 +35,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
|
||||
final timelineOrigin = ref.read(timelineServiceProvider).origin;
|
||||
final showViewInTimelineButton =
|
||||
timelineOrigin != TimelineOrigin.main &&
|
||||
timelineOrigin != TimelineOrigin.deepLink &&
|
||||
timelineOrigin != TimelineOrigin.trash &&
|
||||
timelineOrigin != TimelineOrigin.archive &&
|
||||
timelineOrigin != TimelineOrigin.localAlbum &&
|
||||
isOwner;
|
||||
|
||||
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
|
||||
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
||||
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||
@@ -63,11 +47,10 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
opacity = 0;
|
||||
}
|
||||
|
||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||
final originalTheme = context.themeData;
|
||||
|
||||
final actions = <Widget>[
|
||||
if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||
if (isCasting || (asset.hasRemote)) const CastActionButton(iconOnly: true),
|
||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||
if (album != null && album.isActivityEnabled && album.isShared)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chat_outlined),
|
||||
@@ -75,28 +58,16 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true));
|
||||
},
|
||||
),
|
||||
if (showViewInTimelineButton)
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await context.maybePop();
|
||||
await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
|
||||
EventStream.shared.emit(ScrollToDateEvent(asset.createdAt));
|
||||
},
|
||||
icon: const Icon(Icons.image_search),
|
||||
tooltip: 'view_in_timeline'.t(context: context),
|
||||
),
|
||||
|
||||
if (asset.hasRemote && isOwner && !asset.isFavorite)
|
||||
const FavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||
if (asset.hasRemote && isOwner && asset.isFavorite)
|
||||
const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||
const ViewerKebabMenu(),
|
||||
|
||||
ViewerKebabMenu(originalTheme: originalTheme),
|
||||
];
|
||||
|
||||
final lockedViewActions = <Widget>[
|
||||
if (isCasting || (asset.hasRemote)) const CastActionButton(iconOnly: true),
|
||||
const ViewerKebabMenu(),
|
||||
];
|
||||
final lockedViewActions = <Widget>[ViewerKebabMenu(originalTheme: originalTheme)];
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: opacity < 255,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
|
||||
class ViewerKebabMenu extends ConsumerWidget {
|
||||
const ViewerKebabMenu({super.key});
|
||||
const ViewerKebabMenu({super.key, this.originalTheme});
|
||||
|
||||
final ThemeData? originalTheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -17,25 +20,42 @@ class ViewerKebabMenu extends ConsumerWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final menuChildren = <Widget>[
|
||||
BaseActionButton(
|
||||
label: 'about'.tr(),
|
||||
iconData: Icons.info_outline,
|
||||
menuItem: true,
|
||||
onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()),
|
||||
),
|
||||
];
|
||||
final user = ref.watch(currentUserProvider);
|
||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||
final timelineOrigin = ref.read(timelineServiceProvider).origin;
|
||||
|
||||
final kebabContext = ViewerKebabMenuButtonContext(
|
||||
asset: asset,
|
||||
isOwner: isOwner,
|
||||
isCasting: isCasting,
|
||||
timelineOrigin: timelineOrigin,
|
||||
originalTheme: originalTheme,
|
||||
);
|
||||
|
||||
final menuChildren = ViewerKebabMenuButtonBuilder.build(kebabContext, context, ref);
|
||||
|
||||
return MenuAnchor(
|
||||
consumeOutsideTap: true,
|
||||
style: MenuStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor),
|
||||
surfaceTintColor: const WidgetStatePropertyAll(Colors.grey),
|
||||
elevation: const WidgetStatePropertyAll(4),
|
||||
shape: const WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
|
||||
),
|
||||
menuChildren: menuChildren,
|
||||
menuChildren: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 150),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: menuChildren,
|
||||
),
|
||||
),
|
||||
],
|
||||
builder: (context, controller, child) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.more_vert_rounded),
|
||||
|
||||
@@ -324,7 +324,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
final topPadding = context.padding.top + (widget.appBar == null ? 0 : kToolbarHeight) + 10;
|
||||
|
||||
const scrubberBottomPadding = 100.0;
|
||||
final bottomPadding = context.padding.bottom + (widget.appBar == null ? 0 : scrubberBottomPadding);
|
||||
const bottomSheetOpenModifier = 120.0;
|
||||
final bottomPadding =
|
||||
context.padding.bottom +
|
||||
(widget.appBar == null ? 0 : scrubberBottomPadding) +
|
||||
(isMultiSelectEnabled ? bottomSheetOpenModifier : 0);
|
||||
|
||||
final grid = CustomScrollView(
|
||||
primary: true,
|
||||
@@ -347,7 +351,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
addRepaintBoundaries: false,
|
||||
),
|
||||
),
|
||||
const SliverPadding(padding: EdgeInsets.only(bottom: scrubberBottomPadding)),
|
||||
SliverPadding(padding: EdgeInsets.only(bottom: bottomPadding)),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
@@ -19,6 +28,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class ActionButtonContext {
|
||||
final BaseAsset asset;
|
||||
@@ -164,3 +174,98 @@ class ActionButtonBuilder {
|
||||
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class ViewerKebabMenuButtonContext {
|
||||
final BaseAsset asset;
|
||||
final bool isOwner;
|
||||
final bool isCasting;
|
||||
final TimelineOrigin timelineOrigin;
|
||||
final ThemeData? originalTheme;
|
||||
|
||||
const ViewerKebabMenuButtonContext({
|
||||
required this.asset,
|
||||
required this.isOwner,
|
||||
required this.isCasting,
|
||||
required this.timelineOrigin,
|
||||
this.originalTheme,
|
||||
});
|
||||
}
|
||||
|
||||
enum ViewerKebabMenuButtonType {
|
||||
openInfo,
|
||||
viewInTimeline,
|
||||
cast,
|
||||
download;
|
||||
|
||||
/// Defines which group each button belongs to.
|
||||
/// Buttons in the same group will be displayed together,
|
||||
/// with dividers separating different groups.
|
||||
int get group => switch (this) {
|
||||
ViewerKebabMenuButtonType.openInfo => 0,
|
||||
ViewerKebabMenuButtonType.viewInTimeline => 1,
|
||||
ViewerKebabMenuButtonType.cast => 1,
|
||||
ViewerKebabMenuButtonType.download => 1,
|
||||
};
|
||||
|
||||
bool shouldShow(ViewerKebabMenuButtonContext context) {
|
||||
return switch (this) {
|
||||
ViewerKebabMenuButtonType.openInfo => true,
|
||||
ViewerKebabMenuButtonType.viewInTimeline =>
|
||||
context.timelineOrigin != TimelineOrigin.main &&
|
||||
context.timelineOrigin != TimelineOrigin.deepLink &&
|
||||
context.timelineOrigin != TimelineOrigin.trash &&
|
||||
context.timelineOrigin != TimelineOrigin.archive &&
|
||||
context.timelineOrigin != TimelineOrigin.localAlbum &&
|
||||
context.isOwner,
|
||||
ViewerKebabMenuButtonType.cast => context.isCasting || context.asset.hasRemote,
|
||||
ViewerKebabMenuButtonType.download => context.asset.isRemoteOnly,
|
||||
};
|
||||
}
|
||||
|
||||
ConsumerWidget buildButton(ViewerKebabMenuButtonContext context, BuildContext buildContext) {
|
||||
return switch (this) {
|
||||
ViewerKebabMenuButtonType.openInfo => BaseActionButton(
|
||||
label: 'info'.tr(),
|
||||
iconData: Icons.info_outline,
|
||||
iconColor: context.originalTheme?.iconTheme.color,
|
||||
menuItem: true,
|
||||
onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()),
|
||||
),
|
||||
|
||||
ViewerKebabMenuButtonType.viewInTimeline => BaseActionButton(
|
||||
label: 'view_in_timeline'.t(context: buildContext),
|
||||
iconData: Icons.image_search,
|
||||
iconColor: context.originalTheme?.iconTheme.color,
|
||||
menuItem: true,
|
||||
onPressed: () async {
|
||||
await buildContext.maybePop();
|
||||
await buildContext.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
|
||||
EventStream.shared.emit(ScrollToDateEvent(context.asset.createdAt));
|
||||
},
|
||||
),
|
||||
ViewerKebabMenuButtonType.cast => const CastActionButton(menuItem: true),
|
||||
ViewerKebabMenuButtonType.download => const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ViewerKebabMenuButtonBuilder {
|
||||
static List<Widget> build(ViewerKebabMenuButtonContext context, BuildContext buildContext, WidgetRef ref) {
|
||||
final visibleButtons = ViewerKebabMenuButtonType.values.where((type) => type.shouldShow(context)).toList();
|
||||
|
||||
if (visibleButtons.isEmpty) return [];
|
||||
|
||||
final List<Widget> result = [];
|
||||
int? lastGroup;
|
||||
|
||||
for (final type in visibleButtons) {
|
||||
if (lastGroup != null && type.group != lastGroup) {
|
||||
result.add(const Divider(height: 1));
|
||||
}
|
||||
result.add(type.buildButton(context, buildContext).build(buildContext, ref));
|
||||
lastGroup = type.group;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,26 +2,27 @@ import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
|
||||
class EntitiyCountTile extends StatelessWidget {
|
||||
class EntityCountTile extends StatelessWidget {
|
||||
final int count;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
|
||||
const EntitiyCountTile({super.key, required this.count, required this.label, required this.icon});
|
||||
const EntityCountTile({super.key, required this.count, required this.label, required this.icon});
|
||||
|
||||
String zeroPadding(int number, int targetWidth) {
|
||||
final numStr = number.toString();
|
||||
return numStr.length < targetWidth ? "0" * (targetWidth - numStr.length) : "";
|
||||
}
|
||||
|
||||
int calculateMaxDigits(double availableWidth) {
|
||||
const double charWidth = 11.0;
|
||||
return (availableWidth / charWidth).floor().clamp(1, 8);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final availableWidth = (screenWidth - 32 - 8) / 2;
|
||||
const double charWidth = 11.0;
|
||||
final maxDigits = ((availableWidth - 32) / charWidth).floor().clamp(1, 8);
|
||||
|
||||
return Container(
|
||||
height: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainerLow,
|
||||
@@ -29,7 +30,6 @@ class EntitiyCountTile extends StatelessWidget {
|
||||
border: Border.all(width: 0.5, color: context.colorScheme.outline.withAlpha(25)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Icon and Label
|
||||
@@ -38,33 +38,30 @@ class EntitiyCountTile extends StatelessWidget {
|
||||
children: [
|
||||
Icon(icon, color: context.primaryColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Number
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxDigits = calculateMaxDigits(constraints.maxWidth);
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: zeroPadding(count, maxDigits),
|
||||
style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
|
||||
),
|
||||
TextSpan(
|
||||
text: count.toString(),
|
||||
style: TextStyle(color: context.primaryColor),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: zeroPadding(count, maxDigits),
|
||||
style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
|
||||
),
|
||||
);
|
||||
},
|
||||
TextSpan(
|
||||
text: count.toString(),
|
||||
style: TextStyle(color: context.primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -282,76 +282,87 @@ class _SyncStatsCounts extends ConsumerWidget {
|
||||
_SectionHeaderText(text: "assets".t(context: context)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "local".t(context: context),
|
||||
count: localAssetCount,
|
||||
icon: Icons.smartphone,
|
||||
// 1. Wrap in IntrinsicHeight
|
||||
child: IntrinsicHeight(
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// 2. Stretch children vertically to fill the IntrinsicHeight
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "local".t(context: context),
|
||||
count: localAssetCount,
|
||||
icon: Icons.smartphone,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "remote".t(context: context),
|
||||
count: remoteAssetCount,
|
||||
icon: Icons.cloud,
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "remote".t(context: context),
|
||||
count: remoteAssetCount,
|
||||
icon: Icons.cloud,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_SectionHeaderText(text: "albums".t(context: context)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "local".t(context: context),
|
||||
count: localAlbumCount,
|
||||
icon: Icons.smartphone,
|
||||
child: IntrinsicHeight(
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch, // Added
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "local".t(context: context),
|
||||
count: localAlbumCount,
|
||||
icon: Icons.smartphone,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "remote".t(context: context),
|
||||
count: remoteAlbumCount,
|
||||
icon: Icons.cloud,
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "remote".t(context: context),
|
||||
count: remoteAlbumCount,
|
||||
icon: Icons.cloud,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_SectionHeaderText(text: "other".t(context: context)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "memories".t(context: context),
|
||||
count: memoryCount,
|
||||
icon: Icons.calendar_today,
|
||||
child: IntrinsicHeight(
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch, // Added
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "memories".t(context: context),
|
||||
count: memoryCount,
|
||||
icon: Icons.calendar_today,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "hashed_assets".t(context: context),
|
||||
count: localHashedCount,
|
||||
icon: Icons.tag,
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "hashed_assets".t(context: context),
|
||||
count: localHashedCount,
|
||||
icon: Icons.tag,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// To be removed once the experimental feature is stable
|
||||
@@ -364,26 +375,29 @@ class _SyncStatsCounts extends ConsumerWidget {
|
||||
return counts.when(
|
||||
data: (c) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "local".t(context: context),
|
||||
count: c.total,
|
||||
icon: Icons.delete_outline,
|
||||
child: IntrinsicHeight(
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch, // Added
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "local".t(context: context),
|
||||
count: c.total,
|
||||
icon: Icons.delete_outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "hashed_assets".t(context: context),
|
||||
count: c.hashed,
|
||||
icon: Icons.tag,
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "hashed_assets".t(context: context),
|
||||
count: c.hashed,
|
||||
icon: Icons.tag,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[tools]
|
||||
flutter = "3.35.7"
|
||||
flutter = "3.38.4"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"]
|
||||
version = "1.30.0"
|
||||
|
||||
@@ -6,7 +6,7 @@ version: 2.3.1+3028
|
||||
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
flutter: 3.35.7
|
||||
flutter: 3.38.4
|
||||
|
||||
dependencies:
|
||||
async: ^2.13.0
|
||||
@@ -52,7 +52,7 @@ dependencies:
|
||||
isar_community_flutter_libs: 3.3.0-dev.3
|
||||
local_auth: ^2.3.0
|
||||
logging: ^1.3.0
|
||||
maplibre_gl: ^0.22.0
|
||||
maplibre_gl: 0.24.1
|
||||
|
||||
native_video_player:
|
||||
git:
|
||||
@@ -79,7 +79,7 @@ dependencies:
|
||||
socket_io_client: ^2.0.3+1
|
||||
stream_transform: ^2.1.1
|
||||
thumbhash: 0.1.0+1
|
||||
timezone: ^0.9.4
|
||||
timezone: 0.10.1
|
||||
url_launcher: ^6.3.2
|
||||
uuid: ^4.5.1
|
||||
wakelock_plus: ^1.3.0
|
||||
@@ -88,7 +88,7 @@ dependencies:
|
||||
dev_dependencies:
|
||||
auto_route_generator: ^9.0.0
|
||||
build_runner: ^2.4.8
|
||||
custom_lint: ^0.7.5
|
||||
custom_lint: 0.8.1
|
||||
# Drift generator
|
||||
drift_dev: ^2.26.0
|
||||
fake_async: ^1.3.3
|
||||
|
||||
39
pnpm-lock.yaml
generated
39
pnpm-lock.yaml
generated
@@ -244,8 +244,8 @@ importers:
|
||||
specifier: ^62.0.0
|
||||
version: 62.0.0(eslint@9.39.1(jiti@2.6.1))
|
||||
exiftool-vendored:
|
||||
specifier: ^33.0.0
|
||||
version: 33.5.0
|
||||
specifier: ^34.0.0
|
||||
version: 34.0.0
|
||||
globals:
|
||||
specifier: ^16.0.0
|
||||
version: 16.5.0
|
||||
@@ -428,8 +428,8 @@ importers:
|
||||
specifier: 4.3.3
|
||||
version: 4.3.3
|
||||
exiftool-vendored:
|
||||
specifier: ^33.0.0
|
||||
version: 33.5.0
|
||||
specifier: ^34.0.0
|
||||
version: 34.0.0
|
||||
express:
|
||||
specifier: ^5.1.0
|
||||
version: 5.2.0
|
||||
@@ -3236,6 +3236,7 @@ packages:
|
||||
'@koa/router@14.0.0':
|
||||
resolution: {integrity: sha512-LBSu5K0qAaaQcXX/0WIB9PGDevyCxxpnc1uq13vV/CgObaVxuis5hKl3Eboq/8gcb6ebnkAStW9NB/Em2eYyFA==}
|
||||
engines: {node: '>= 20'}
|
||||
deprecated: Please upgrade to v15 or higher. All reported bugs in this version are fixed in newer releases, dependencies have been updated, and security has been improved.
|
||||
|
||||
'@koddsson/eslint-plugin-tscompat@0.2.0':
|
||||
resolution: {integrity: sha512-Oqd4kWSX0LiO9wWHjcmDfXZNC7TotFV/tLRhwCFU3XUeb//KYvJ75c9OmeSJ+vBv5lkCeB+xYsqyNrBc5j18XA==}
|
||||
@@ -5503,8 +5504,8 @@ packages:
|
||||
resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==}
|
||||
hasBin: true
|
||||
|
||||
batch-cluster@15.0.1:
|
||||
resolution: {integrity: sha512-eUmh0ld1AUPKTEmdzwGF9QTSexXAyt9rA1F5zDfW1wUi3okA3Tal4NLdCeFI6aiKpBenQhR6NmK9bW9tBHTGPQ==}
|
||||
batch-cluster@16.0.0:
|
||||
resolution: {integrity: sha512-+T7Ho09ikx/kP4P8M+GEnpuePzRQa4gTUhtPIu6ApFC8+0GY0sri1y1PuB+yfXlQWl5DkHC/e58z3U6g0qCz/A==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
batch@0.6.1:
|
||||
@@ -6848,17 +6849,17 @@ packages:
|
||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
exiftool-vendored.exe@13.42.0:
|
||||
resolution: {integrity: sha512-6AFybe5IakduMWleuQBfep9OWGSVZSedt2uKL+LzufRsATp+beOF7tZyKtMztjb6VRH1GF/4F9EvBVam6zm70w==}
|
||||
exiftool-vendored.exe@13.43.0:
|
||||
resolution: {integrity: sha512-EENHNz86tYY5yHGPtGB2mto3FIGstQvEhrcU34f7fm4RMxBKNfTWYOGkhU1jzvjOi+V4575LQX/FUES1TwgUbQ==}
|
||||
os: [win32]
|
||||
|
||||
exiftool-vendored.pl@13.42.0:
|
||||
resolution: {integrity: sha512-EF5IdxQNIJIvZjHf4bG4jnwAHVVSLkYZToo2q+Mm89kSuppKfRvHz/lngIxN0JALE8rFdC4zt6NWY/PKqRdCcg==}
|
||||
exiftool-vendored.pl@13.43.0:
|
||||
resolution: {integrity: sha512-0ApWaQ/pxaliPK7HzTxVA0sg/wZ8vl7UtFVhCyWhGQg01WfZkFrKwKmELB0Bnn01WTfgIuMadba8ccmFvpmJag==}
|
||||
os: ['!win32']
|
||||
hasBin: true
|
||||
|
||||
exiftool-vendored@33.5.0:
|
||||
resolution: {integrity: sha512-7cCh6izwdmC5ZaCxpHFehnExIr2Yp7CJuxHg4WFiGcm81yyxXLtvSE+85ep9VsNwhlOtSpk+XxiqrlddjY5lAw==}
|
||||
exiftool-vendored@34.0.0:
|
||||
resolution: {integrity: sha512-rhIe4XGE7kh76nwytwHtq6qK/pc1mpOBHRV++gudFeG2PfAp3XIVQbFWCLK3S4l9I4AWYOe4mxk8mW8l1oHRTw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
expect-type@1.2.1:
|
||||
@@ -17580,7 +17581,7 @@ snapshots:
|
||||
|
||||
baseline-browser-mapping@2.8.31: {}
|
||||
|
||||
batch-cluster@15.0.1: {}
|
||||
batch-cluster@16.0.0: {}
|
||||
|
||||
batch@0.6.1: {}
|
||||
|
||||
@@ -19128,21 +19129,21 @@ snapshots:
|
||||
signal-exit: 3.0.7
|
||||
strip-final-newline: 2.0.0
|
||||
|
||||
exiftool-vendored.exe@13.42.0:
|
||||
exiftool-vendored.exe@13.43.0:
|
||||
optional: true
|
||||
|
||||
exiftool-vendored.pl@13.42.0: {}
|
||||
exiftool-vendored.pl@13.43.0: {}
|
||||
|
||||
exiftool-vendored@33.5.0:
|
||||
exiftool-vendored@34.0.0:
|
||||
dependencies:
|
||||
'@photostructure/tz-lookup': 11.3.0
|
||||
'@types/luxon': 3.7.1
|
||||
batch-cluster: 15.0.1
|
||||
exiftool-vendored.pl: 13.42.0
|
||||
batch-cluster: 16.0.0
|
||||
exiftool-vendored.pl: 13.43.0
|
||||
he: 1.2.0
|
||||
luxon: 3.7.2
|
||||
optionalDependencies:
|
||||
exiftool-vendored.exe: 13.42.0
|
||||
exiftool-vendored.exe: 13.43.0
|
||||
|
||||
expect-type@1.2.1: {}
|
||||
|
||||
|
||||
@@ -50,13 +50,15 @@ RUN --mount=type=cache,id=pnpm-cli,target=/buildcache/pnpm-store \
|
||||
|
||||
FROM builder AS plugins
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2025.11.3@sha256:ac26f5978c0e2783f3e68e58ce75eddb83e41b89bf8747c503bac2aa9baf22c5 /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY ./plugins/mise.toml ./plugins/
|
||||
ENV MISE_TRUSTED_CONFIG_PATHS=/usr/src/app/plugins/mise.toml
|
||||
ENV MISE_DATA_DIR=/buildcache/mise
|
||||
RUN --mount=type=cache,id=mise-tools,target=/buildcache/mise \
|
||||
RUN --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
mise install --cd plugins
|
||||
|
||||
COPY ./plugins ./plugins/
|
||||
@@ -66,7 +68,7 @@ RUN --mount=type=cache,id=pnpm-plugins,target=/buildcache/pnpm-store \
|
||||
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
|
||||
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
|
||||
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
|
||||
--mount=type=cache,id=mise-tools,target=/buildcache/mise \
|
||||
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
cd plugins && mise run build
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"cookie": "^1.0.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cron": "4.3.3",
|
||||
"exiftool-vendored": "^33.0.0",
|
||||
"exiftool-vendored": "^34.0.0",
|
||||
"express": "^5.1.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</script>
|
||||
|
||||
<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}`))}
|
||||
{oncontextmenu}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
||||
import { preloadManager } from '$lib/managers/PreloadManager.svelte';
|
||||
import { cancelImageUrl } from '$lib/utils/sw-messaging';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiEyeOffOutline } from '@mdi/js';
|
||||
import type { ActionReturn } from 'svelte/action';
|
||||
@@ -60,7 +60,7 @@
|
||||
onComplete?.(false);
|
||||
}
|
||||
return {
|
||||
destroy: () => preloadManager.cancelPreloadUrl(url),
|
||||
destroy: () => cancelImageUrl(url),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
|
||||
const onMouseLeave = () => {
|
||||
mouseOver = false;
|
||||
onMouseEvent?.({ isMouseOver: false, selectedGroupIndex: groupIndex });
|
||||
};
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { getAssetUrl } from '$lib/utils';
|
||||
import { cancelImageUrl, preloadImageUrl } from '$lib/utils/sw-messaging';
|
||||
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
||||
|
||||
class PreloadManager {
|
||||
preload(asset: AssetResponseDto | undefined) {
|
||||
if (!asset) {
|
||||
return;
|
||||
}
|
||||
if (globalThis.isSecureContext) {
|
||||
preloadImageUrl(getAssetUrl({ asset }));
|
||||
return;
|
||||
}
|
||||
if (asset.type === AssetTypeEnum.Image) {
|
||||
const img = new Image();
|
||||
img.src = getAssetUrl({ asset });
|
||||
}
|
||||
}
|
||||
|
||||
cancel(asset: AssetResponseDto | undefined) {
|
||||
if (!globalThis.isSecureContext || !asset) {
|
||||
return;
|
||||
}
|
||||
const url = getAssetUrl({ asset });
|
||||
cancelImageUrl(url);
|
||||
}
|
||||
|
||||
cancelPreloadUrl(url: string) {
|
||||
if (!globalThis.isSecureContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelImageUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
export const preloadManager = new PreloadManager();
|
||||
@@ -1,12 +1,10 @@
|
||||
import { defaultLang, langs, locales } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { alwaysLoadOriginalFile, lang } from '$lib/stores/preferences.store';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { lang } from '$lib/stores/preferences.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import {
|
||||
AssetJobName,
|
||||
AssetMediaSize,
|
||||
AssetTypeEnum,
|
||||
MemoryType,
|
||||
QueueName,
|
||||
finishOAuth,
|
||||
@@ -19,7 +17,6 @@ import {
|
||||
linkOAuthAccount,
|
||||
startOAuth,
|
||||
unlinkOAuthAccount,
|
||||
type AssetResponseDto,
|
||||
type MemoryResponseDto,
|
||||
type PersonResponseDto,
|
||||
type ServerVersionResponseDto,
|
||||
@@ -194,37 +191,6 @@ const createUrl = (path: string, parameters?: Record<string, unknown>) => {
|
||||
|
||||
type AssetUrlOptions = { id: string; cacheKey?: string | null };
|
||||
|
||||
export const getAssetUrl = ({
|
||||
asset,
|
||||
sharedLink,
|
||||
forceOriginal = false,
|
||||
}: {
|
||||
asset: AssetResponseDto;
|
||||
sharedLink?: SharedLinkResponseDto;
|
||||
forceOriginal?: boolean;
|
||||
}) => {
|
||||
const id = asset.id;
|
||||
const cacheKey = asset.thumbhash;
|
||||
if (sharedLink && (!sharedLink.allowDownload || !sharedLink.showMetadata)) {
|
||||
return getAssetThumbnailUrl({ id, size: AssetMediaSize.Preview, cacheKey });
|
||||
}
|
||||
const targetSize = targetImageSize(asset, forceOriginal);
|
||||
return targetSize === 'original'
|
||||
? getAssetOriginalUrl({ id, cacheKey })
|
||||
: getAssetThumbnailUrl({ id, size: targetSize, cacheKey });
|
||||
};
|
||||
|
||||
const forceUseOriginal = (asset: AssetResponseDto) => {
|
||||
return asset.type === AssetTypeEnum.Image && asset.duration && !asset.duration.includes('0:00:00.000');
|
||||
};
|
||||
|
||||
export const targetImageSize = (asset: AssetResponseDto, forceOriginal: boolean) => {
|
||||
if (forceOriginal || get(alwaysLoadOriginalFile) || forceUseOriginal(asset)) {
|
||||
return isWebCompatibleImage(asset) ? 'original' : AssetMediaSize.Fullsize;
|
||||
}
|
||||
return AssetMediaSize.Preview;
|
||||
};
|
||||
|
||||
export const getAssetOriginalUrl = (options: string | AssetUrlOptions) => {
|
||||
if (typeof options === 'string') {
|
||||
options = { id: options };
|
||||
|
||||
Reference in New Issue
Block a user