Compare commits

..

1 Commits

Author SHA1 Message Date
Jonathan Jogenfors
38b58902fe feat(web): loop chromecast video 2025-12-05 22:17:51 +01:00
29 changed files with 149 additions and 297 deletions

View File

@@ -222,7 +222,6 @@ jobs:
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
working-directory: ./mobile/ios
- name: Install CocoaPods dependencies
@@ -230,6 +229,13 @@ 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 }}

View File

@@ -32,6 +32,8 @@ server {
# enable websockets: http://nginx.org/en/docs/http/websocket.html
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
# set timeout
@@ -41,8 +43,6 @@ server {
location / {
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

View File

@@ -36,7 +36,7 @@
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^62.0.0",
"exiftool-vendored": "^34.0.0",
"exiftool-vendored": "^33.0.0",
"globals": "^16.0.0",
"jose": "^5.6.3",
"luxon": "^3.4.4",

View File

@@ -6,7 +6,6 @@ class ExifInfo {
final String? orientation;
final String? timeZone;
final DateTime? dateTimeOriginal;
final int? rating;
// GPS
final double? latitude;
@@ -47,7 +46,6 @@ class ExifInfo {
this.orientation,
this.timeZone,
this.dateTimeOriginal,
this.rating,
this.isFlipped = false,
this.latitude,
this.longitude,
@@ -73,7 +71,6 @@ class ExifInfo {
other.orientation == orientation &&
other.timeZone == timeZone &&
other.dateTimeOriginal == dateTimeOriginal &&
other.rating == rating &&
other.latitude == latitude &&
other.longitude == longitude &&
other.city == city &&
@@ -97,7 +94,6 @@ class ExifInfo {
isFlipped.hashCode ^
timeZone.hashCode ^
dateTimeOriginal.hashCode ^
rating.hashCode ^
latitude.hashCode ^
longitude.hashCode ^
city.hashCode ^
@@ -122,7 +118,6 @@ orientation: ${orientation ?? 'NA'},
isFlipped: $isFlipped,
timeZone: ${timeZone ?? 'NA'},
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
rating: ${rating ?? 'NA'},
latitude: ${latitude ?? 'NA'},
longitude: ${longitude ?? 'NA'},
city: ${city ?? 'NA'},
@@ -145,7 +140,6 @@ exposureSeconds: ${exposureSeconds ?? 'NA'},
String? orientation,
String? timeZone,
DateTime? dateTimeOriginal,
int? rating,
double? latitude,
double? longitude,
String? city,
@@ -167,7 +161,6 @@ exposureSeconds: ${exposureSeconds ?? 'NA'},
orientation: orientation ?? this.orientation,
timeZone: timeZone ?? this.timeZone,
dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal,
rating: rating ?? this.rating,
isFlipped: isFlipped ?? this.isFlipped,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,

View File

@@ -151,7 +151,6 @@ extension RemoteExifEntityDataDomainEx on RemoteExifEntityData {
domain.ExifInfo toDto() => domain.ExifInfo(
fileSize: fileSize,
dateTimeOriginal: dateTimeOriginal,
rating: rating,
timeZone: timeZone,
make: make,
model: model,

View File

@@ -255,12 +255,6 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
);
}
Future<void> updateRating(String assetId, int rating) async {
await (_db.remoteExifEntity.update()..where((row) => row.assetId.equals(assetId))).write(
RemoteExifEntityCompanion(rating: Value(rating)),
);
}
Future<int> getCount() {
return _db.managers.remoteAssetEntity.count();
}

View File

@@ -37,7 +37,7 @@ class DriftActivitiesPage extends HookConsumerWidget {
child: Scaffold(
appBar: AppBar(
title: Text(album.name),
actions: [const LikeActivityActionButton(iconOnly: true)],
actions: [const LikeActivityActionButton(menuItem: true)],
actionsPadding: const EdgeInsets.only(right: 8),
),
body: activities.widgetWhen(

View File

@@ -21,34 +21,12 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee
enum AddToMenuItem { album, archive, unarchive, lockedFolder }
class AddActionButton extends ConsumerStatefulWidget {
class AddActionButton extends ConsumerWidget {
const AddActionButton({super.key});
@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() {
Future<void> _showAddOptions(BuildContext context, WidgetRef ref) async {
final asset = ref.read(currentAssetNotifier);
if (asset == null) return [];
if (asset == null) return;
final user = ref.read(currentUserProvider);
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
@@ -57,57 +35,93 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
final hasRemote = asset is RemoteAsset;
final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived;
final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived;
final menuItemHeight = 30.0;
return [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text("add_to_bottom_bar".tr(), style: context.textTheme.labelMedium),
final List<PopupMenuEntry<AddToMenuItem>> items = [
PopupMenuItem(
enabled: false,
textStyle: context.textTheme.labelMedium,
height: 40,
child: Text("add_to_bottom_bar".tr()),
),
BaseActionButton(
iconData: Icons.photo_album_outlined,
label: "album".tr(),
menuItem: true,
onPressed: () => _handleMenuSelection(AddToMenuItem.album),
PopupMenuItem(
height: menuItemHeight,
value: AddToMenuItem.album,
child: ListTile(leading: const Icon(Icons.photo_album_outlined), title: Text("album".tr())),
),
const PopupMenuDivider(),
PopupMenuItem(enabled: false, textStyle: context.textTheme.labelMedium, height: 40, child: Text("move_to".tr())),
if (isOwner) ...[
const PopupMenuDivider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text("move_to".tr(), style: context.textTheme.labelMedium),
),
if (showArchive)
BaseActionButton(
iconData: Icons.archive_outlined,
label: "archive".tr(),
menuItem: true,
onPressed: () => _handleMenuSelection(AddToMenuItem.archive),
PopupMenuItem(
height: menuItemHeight,
value: AddToMenuItem.archive,
child: ListTile(leading: const Icon(Icons.archive_outlined), title: Text("archive".tr())),
),
if (showUnarchive)
BaseActionButton(
iconData: Icons.unarchive_outlined,
label: "unarchive".tr(),
menuItem: true,
onPressed: () => _handleMenuSelection(AddToMenuItem.unarchive),
PopupMenuItem(
height: menuItemHeight,
value: AddToMenuItem.unarchive,
child: ListTile(leading: const Icon(Icons.unarchive_outlined), title: Text("unarchive".tr())),
),
BaseActionButton(
iconData: Icons.lock_outline,
label: "locked_folder".tr(),
menuItem: true,
onPressed: () => _handleMenuSelection(AddToMenuItem.lockedFolder),
PopupMenuItem(
height: menuItemHeight,
value: AddToMenuItem.lockedFolder,
child: ListTile(leading: const Icon(Icons.lock_outline), title: Text("locked_folder".tr())),
),
],
];
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;
}
}
void _openAlbumSelector() {
RelativeRect _menuPosition(BuildContext context) {
final renderObject = context.findRenderObject();
if (renderObject is! RenderBox) {
return RelativeRect.fill;
}
final size = renderObject.size;
final position = renderObject.localToGlobal(Offset.zero);
return RelativeRect.fromLTRB(position.dx, position.dy - size.height - 200, position.dx + size.width, position.dy);
}
void _openAlbumSelector(BuildContext context, WidgetRef ref) {
final currentAsset = ref.read(currentAssetNotifier);
if (currentAsset == null) {
ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error);
return;
}
final List<Widget> slivers = [AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album))];
final List<Widget> slivers = [
AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(context, ref, album)),
];
showModalBottomSheet(
context: context,
@@ -127,7 +141,7 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
);
}
Future<void> _addCurrentAssetToAlbum(RemoteAlbum album) async {
Future<void> _addCurrentAssetToAlbum(BuildContext context, WidgetRef ref, RemoteAlbum album) async {
final latest = ref.read(currentAssetNotifier);
if (latest == null) {
@@ -160,27 +174,17 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier);
if (asset == null) {
return const SizedBox.shrink();
}
return MenuAnchor(
consumeOutsideTap: true,
style: MenuStyle(
backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor),
elevation: const WidgetStatePropertyAll(4),
shape: const WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
),
menuChildren: _buildMenuChildren(),
builder: (context, controller, child) {
return Builder(
builder: (buttonContext) {
return BaseActionButton(
iconData: Icons.add,
label: "add_to_bottom_bar".tr(),
onPressed: () => controller.isOpen ? controller.close() : controller.open(),
onPressed: () => _showAddOptions(buttonContext, ref),
);
},
);

View File

@@ -11,7 +11,6 @@ class BaseActionButton extends StatelessWidget {
this.onLongPressed,
this.maxWidth = 90.0,
this.minWidth,
this.iconOnly = false,
this.menuItem = false,
});
@@ -20,11 +19,6 @@ class BaseActionButton extends StatelessWidget {
final Color? iconColor;
final double maxWidth;
final double? minWidth;
/// When true, renders only an IconButton without text label
final bool iconOnly;
/// When true, renders as a MenuItemButton for use in MenuAnchor menus
final bool menuItem;
final void Function()? onPressed;
final void Function()? onLongPressed;
@@ -37,26 +31,13 @@ class BaseActionButton extends StatelessWidget {
final iconColor = this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color;
final textColor = context.themeData.textTheme.labelLarge?.color;
if (iconOnly) {
if (menuItem) {
return IconButton(
onPressed: onPressed,
icon: Icon(iconData, size: iconSize, color: iconColor),
);
}
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),
onPressed: onPressed,
child: Text(label, style: effectiveStyle),
);
}
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: MaterialButton(

View File

@@ -7,9 +7,8 @@ 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.menuItem = true});
final bool iconOnly;
final bool menuItem;
@override
@@ -23,7 +22,6 @@ class CastActionButton extends ConsumerWidget {
onPressed: () {
showDialog(context: context, builder: (context) => const CastDialog());
},
iconOnly: iconOnly,
menuItem: menuItem,
);
}

View File

@@ -10,9 +10,8 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class DownloadActionButton extends ConsumerWidget {
final ActionSource source;
final bool iconOnly;
final bool menuItem;
const DownloadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
const DownloadActionButton({super.key, required this.source, this.menuItem = false});
void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async {
if (!context.mounted) {
@@ -39,7 +38,6 @@ class DownloadActionButton extends ConsumerWidget {
iconData: Icons.download,
maxWidth: 95,
label: "download".t(context: context),
iconOnly: iconOnly,
menuItem: menuItem,
onPressed: () => _onTap(context, ref, backgroundManager),
);

View File

@@ -10,10 +10,9 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
class FavoriteActionButton extends ConsumerWidget {
final ActionSource source;
final bool iconOnly;
final bool menuItem;
const FavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
const FavoriteActionButton({super.key, required this.source, this.menuItem = false});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
@@ -45,7 +44,6 @@ class FavoriteActionButton extends ConsumerWidget {
return BaseActionButton(
iconData: Icons.favorite_border_rounded,
label: "favorite".t(context: context),
iconOnly: iconOnly,
menuItem: menuItem,
onPressed: () => _onTap(context, ref),
);

View File

@@ -12,9 +12,8 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
import 'package:immich_mobile/providers/user.provider.dart';
class LikeActivityActionButton extends ConsumerWidget {
const LikeActivityActionButton({super.key, this.iconOnly = false, this.menuItem = false});
const LikeActivityActionButton({super.key, this.menuItem = false});
final bool iconOnly;
final bool menuItem;
@override
@@ -50,7 +49,6 @@ class LikeActivityActionButton extends ConsumerWidget {
iconData: liked != null ? Icons.favorite : Icons.favorite_border,
label: "like".t(context: context),
onPressed: () => onTap(liked),
iconOnly: iconOnly,
menuItem: menuItem,
);
},
@@ -59,7 +57,6 @@ class LikeActivityActionButton extends ConsumerWidget {
loading: () => BaseActionButton(
iconData: Icons.favorite_border,
label: "like".t(context: context),
iconOnly: iconOnly,
menuItem: menuItem,
),
error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])),

View File

@@ -5,9 +5,8 @@ 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.menuItem = true});
final bool iconOnly;
final bool menuItem;
@override
@@ -18,7 +17,6 @@ class MotionPhotoActionButton extends ConsumerWidget {
iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded,
label: "play_motion_photo".t(context: context),
onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle,
iconOnly: iconOnly,
menuItem: menuItem,
);
}

View File

@@ -10,10 +10,9 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
class UnFavoriteActionButton extends ConsumerWidget {
final ActionSource source;
final bool iconOnly;
final bool menuItem;
const UnFavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
const UnFavoriteActionButton({super.key, required this.source, this.menuItem = false});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
@@ -46,7 +45,6 @@ class UnFavoriteActionButton extends ConsumerWidget {
iconData: Icons.favorite_rounded,
label: "unfavorite".t(context: context),
onPressed: () => _onTap(context, ref),
iconOnly: iconOnly,
menuItem: menuItem,
);
}

View File

@@ -4,7 +4,6 @@ import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
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';
@@ -24,7 +23,6 @@ import 'package:immich_mobile/providers/infrastructure/album.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/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@@ -56,12 +54,6 @@ class AssetDetailBottomSheet extends ConsumerWidget {
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
final isRatingEnabled = ref
.watch(userMetadataProvider(ref.watch(currentUserProvider)?.id ?? ''))
.maybeWhen(
data: (metadataList) => metadataList.any((meta) => meta.preferences?.ratingsEnabled ?? false),
orElse: () => false,
);
final buttonContext = ActionButtonContext(
asset: asset,
@@ -79,7 +71,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
return BaseBottomSheet(
actions: actions,
slivers: [_AssetDetailBottomSheet(isRatingEnabled: isRatingEnabled)],
slivers: const [_AssetDetailBottomSheet()],
controller: controller,
initialChildSize: initialChildSize,
minChildSize: 0.1,
@@ -93,9 +85,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
}
class _AssetDetailBottomSheet extends ConsumerWidget {
final bool isRatingEnabled;
const _AssetDetailBottomSheet({required this.isRatingEnabled});
const _AssetDetailBottomSheet();
String _getDateTime(BuildContext ctx, BaseAsset asset, ExifInfo? exifInfo) {
DateTime dateTime = asset.createdAt.toLocal();
@@ -333,36 +323,6 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
),
),
],
// Rating bar
if (isRatingEnabled) ...[
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Text(
'rating'.t(context: context).toUpperCase(),
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
RatingBar.builder(
initialRating: exifInfo?.rating?.toDouble() ?? 0,
itemBuilder: (context, _) => Icon(Icons.star, color: context.themeData.colorScheme.primary),
itemSize: 32,
glow: false,
onRatingUpdate: (rating) async {
await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, rating.round());
},
),
],
),
),
],
// Appears in (Albums)
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
// padding at the bottom to avoid cut-off

View File

@@ -14,7 +14,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_actio
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';
@@ -66,8 +65,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
final actions = <Widget>[
if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, iconOnly: true),
if (isCasting || (asset.hasRemote)) const CastActionButton(iconOnly: true),
if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
if (album != null && album.isActivityEnabled && album.isShared)
IconButton(
icon: const Icon(Icons.chat_outlined),
@@ -86,16 +85,16 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
tooltip: 'view_in_timeline'.t(context: context),
),
if (asset.hasRemote && isOwner && !asset.isFavorite)
const FavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
const FavoriteActionButton(source: ActionSource.viewer, menuItem: true),
if (asset.hasRemote && isOwner && asset.isFavorite)
const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
const ViewerKebabMenu(),
const UnFavoriteActionButton(source: ActionSource.viewer, menuItem: true),
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
const _KebabMenu(),
];
final lockedViewActions = <Widget>[
if (isCasting || (asset.hasRemote)) const CastActionButton(iconOnly: true),
const ViewerKebabMenu(),
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
const _KebabMenu(),
];
return IgnorePointer(
@@ -123,6 +122,20 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
Size get preferredSize => const Size.fromHeight(60.0);
}
class _KebabMenu extends ConsumerWidget {
const _KebabMenu();
@override
Widget build(BuildContext context, WidgetRef ref) {
return IconButton(
onPressed: () {
EventStream.shared.emit(const ViewerOpenBottomSheetEvent());
},
icon: const Icon(Icons.more_vert_rounded),
);
}
}
class _AppBarBackButton extends ConsumerWidget {
const _AppBarBackButton();

View File

@@ -1,47 +0,0 @@
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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
class ViewerKebabMenu extends ConsumerWidget {
const ViewerKebabMenu({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier);
if (asset == null) {
return const SizedBox.shrink();
}
final menuChildren = <Widget>[
BaseActionButton(
label: 'about'.tr(),
iconData: Icons.info_outline,
menuItem: true,
onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()),
),
];
return MenuAnchor(
consumeOutsideTap: true,
style: MenuStyle(
backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor),
elevation: const WidgetStatePropertyAll(4),
shape: const WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
),
menuChildren: menuChildren,
builder: (context, controller, child) {
return IconButton(
icon: const Icon(Icons.more_vert_rounded),
onPressed: () => controller.isOpen ? controller.close() : controller.open(),
);
},
);
}
}

View File

@@ -357,22 +357,6 @@ class ActionNotifier extends Notifier<void> {
}
}
Future<ActionResult> updateRating(ActionSource source, int rating) async {
final ids = _getRemoteIdsForSource(source);
if (ids.length != 1) {
_logger.warning('updateRating called with multiple assets, expected single asset');
return ActionResult(count: ids.length, success: false, error: 'Expected single asset for rating update');
}
try {
final isUpdated = await _service.updateRating(ids.first, rating);
return ActionResult(count: 1, success: isUpdated);
} catch (error, stack) {
_logger.severe('Failed to update rating for asset', error, stack);
return ActionResult(count: 1, success: false, error: error.toString());
}
}
Future<ActionResult> stack(String userId, ActionSource source) async {
final ids = _getOwnedRemoteIdsForSource(source);
try {

View File

@@ -1,13 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final userMetadataRepository = Provider<DriftUserMetadataRepository>(
(ref) => DriftUserMetadataRepository(ref.watch(driftProvider)),
);
final userMetadataProvider = FutureProvider.family<List<UserMetadata>, String>((ref, String userId) async {
final repository = ref.watch(userMetadataRepository);
return repository.getUserMetadata(userId);
});

View File

@@ -101,10 +101,6 @@ class AssetApiRepository extends ApiRepository {
Future<void> updateDescription(String assetId, String description) {
return _api.updateAsset(assetId, UpdateAssetDto(description: description));
}
Future<void> updateRating(String assetId, int rating) {
return _api.updateAsset(assetId, UpdateAssetDto(rating: rating));
}
}
extension on StackResponseDto {

View File

@@ -167,7 +167,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]),
AutoRoute(page: ChangePasswordRoute.page),
AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false),
AutoRoute(
CustomRoute(
page: TabControllerRoute.page,
guards: [_authGuard, _duplicateGuard],
children: [
@@ -176,8 +176,9 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
],
transitionsBuilder: TransitionsBuilders.fadeIn,
),
AutoRoute(
CustomRoute(
page: TabShellRoute.page,
guards: [_authGuard, _duplicateGuard],
children: [
@@ -186,6 +187,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftLibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
],
transitionsBuilder: TransitionsBuilders.fadeIn,
),
CustomRoute(
page: GalleryViewerRoute.page,

View File

@@ -225,14 +225,6 @@ class ActionService {
return true;
}
Future<bool> updateRating(String assetId, int rating) async {
// update remote first, then local to ensure consistency
await _assetApiRepository.updateRating(assetId, rating);
await _remoteAssetRepository.updateRating(assetId, rating);
return true;
}
Future<void> stack(String userId, List<String> remoteIds) async {
final stack = await _assetApiRepository.stack(remoteIds);
await _remoteAssetRepository.stack(userId, stack);

View File

@@ -665,14 +665,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.27"
flutter_rating_bar:
dependency: "direct main"
description:
name: flutter_rating_bar
sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93
url: "https://pub.dev"
source: hosted
version: "4.0.1"
flutter_riverpod:
dependency: transitive
description:

View File

@@ -33,7 +33,6 @@ dependencies:
flutter_displaymode: ^0.7.0
flutter_hooks: ^0.21.3+1
flutter_local_notifications: ^17.2.1+2
flutter_rating_bar: ^4.0.1
flutter_secure_storage: ^9.2.4
flutter_svg: ^2.2.1
flutter_udid: ^4.0.0

39
pnpm-lock.yaml generated
View File

@@ -244,8 +244,8 @@ importers:
specifier: ^62.0.0
version: 62.0.0(eslint@9.39.1(jiti@2.6.1))
exiftool-vendored:
specifier: ^34.0.0
version: 34.0.0
specifier: ^33.0.0
version: 33.5.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: ^34.0.0
version: 34.0.0
specifier: ^33.0.0
version: 33.5.0
express:
specifier: ^5.1.0
version: 5.2.0
@@ -3236,7 +3236,6 @@ 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==}
@@ -5504,8 +5503,8 @@ packages:
resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==}
hasBin: true
batch-cluster@16.0.0:
resolution: {integrity: sha512-+T7Ho09ikx/kP4P8M+GEnpuePzRQa4gTUhtPIu6ApFC8+0GY0sri1y1PuB+yfXlQWl5DkHC/e58z3U6g0qCz/A==}
batch-cluster@15.0.1:
resolution: {integrity: sha512-eUmh0ld1AUPKTEmdzwGF9QTSexXAyt9rA1F5zDfW1wUi3okA3Tal4NLdCeFI6aiKpBenQhR6NmK9bW9tBHTGPQ==}
engines: {node: '>=20'}
batch@0.6.1:
@@ -6849,17 +6848,17 @@ packages:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
exiftool-vendored.exe@13.43.0:
resolution: {integrity: sha512-EENHNz86tYY5yHGPtGB2mto3FIGstQvEhrcU34f7fm4RMxBKNfTWYOGkhU1jzvjOi+V4575LQX/FUES1TwgUbQ==}
exiftool-vendored.exe@13.42.0:
resolution: {integrity: sha512-6AFybe5IakduMWleuQBfep9OWGSVZSedt2uKL+LzufRsATp+beOF7tZyKtMztjb6VRH1GF/4F9EvBVam6zm70w==}
os: [win32]
exiftool-vendored.pl@13.43.0:
resolution: {integrity: sha512-0ApWaQ/pxaliPK7HzTxVA0sg/wZ8vl7UtFVhCyWhGQg01WfZkFrKwKmELB0Bnn01WTfgIuMadba8ccmFvpmJag==}
exiftool-vendored.pl@13.42.0:
resolution: {integrity: sha512-EF5IdxQNIJIvZjHf4bG4jnwAHVVSLkYZToo2q+Mm89kSuppKfRvHz/lngIxN0JALE8rFdC4zt6NWY/PKqRdCcg==}
os: ['!win32']
hasBin: true
exiftool-vendored@34.0.0:
resolution: {integrity: sha512-rhIe4XGE7kh76nwytwHtq6qK/pc1mpOBHRV++gudFeG2PfAp3XIVQbFWCLK3S4l9I4AWYOe4mxk8mW8l1oHRTw==}
exiftool-vendored@33.5.0:
resolution: {integrity: sha512-7cCh6izwdmC5ZaCxpHFehnExIr2Yp7CJuxHg4WFiGcm81yyxXLtvSE+85ep9VsNwhlOtSpk+XxiqrlddjY5lAw==}
engines: {node: '>=20.0.0'}
expect-type@1.2.1:
@@ -17581,7 +17580,7 @@ snapshots:
baseline-browser-mapping@2.8.31: {}
batch-cluster@16.0.0: {}
batch-cluster@15.0.1: {}
batch@0.6.1: {}
@@ -19129,21 +19128,21 @@ snapshots:
signal-exit: 3.0.7
strip-final-newline: 2.0.0
exiftool-vendored.exe@13.43.0:
exiftool-vendored.exe@13.42.0:
optional: true
exiftool-vendored.pl@13.43.0: {}
exiftool-vendored.pl@13.42.0: {}
exiftool-vendored@34.0.0:
exiftool-vendored@33.5.0:
dependencies:
'@photostructure/tz-lookup': 11.3.0
'@types/luxon': 3.7.1
batch-cluster: 16.0.0
exiftool-vendored.pl: 13.43.0
batch-cluster: 15.0.1
exiftool-vendored.pl: 13.42.0
he: 1.2.0
luxon: 3.7.2
optionalDependencies:
exiftool-vendored.exe: 13.43.0
exiftool-vendored.exe: 13.42.0
expect-type@1.2.1: {}

View File

@@ -70,7 +70,7 @@
"cookie": "^1.0.2",
"cookie-parser": "^1.4.7",
"cron": "4.3.3",
"exiftool-vendored": "^34.0.0",
"exiftool-vendored": "^33.0.0",
"express": "^5.1.0",
"fast-glob": "^3.3.2",
"fluent-ffmpeg": "^2.1.2",

View File

@@ -126,7 +126,6 @@
const onMouseLeave = () => {
mouseOver = false;
onMouseEvent?.({ isMouseOver: false, selectedGroupIndex: groupIndex });
};
let timer: ReturnType<typeof setTimeout> | null = null;

View File

@@ -115,12 +115,17 @@ export class GCastDestination implements ICastDestination {
// build the authenticated media request and send it to the cast device
const authenticatedUrl = `${mediaUrl}&sessionKey=${sessionKey}`;
const mediaInfo = new chrome.cast.media.MediaInfo(authenticatedUrl, contentType);
const request = new chrome.cast.media.LoadRequest(mediaInfo);
// Create a queue with a single item and set it to repeat
const queueItem = new chrome.cast.media.QueueItem(mediaInfo);
const queueLoadRequest = new chrome.cast.media.QueueLoadRequest([queueItem]);
queueLoadRequest.repeatMode = chrome.cast.media.RepeatMode.SINGLE;
const successCallback = this.onMediaDiscovered.bind(this, SESSION_DISCOVERY_CAUSE.LOAD_MEDIA);
this.currentUrl = mediaUrl;
return this.session.loadMedia(request, successCallback, this.onError.bind(this));
return this.session.queueLoad(queueLoadRequest, successCallback, this.onError.bind(this));
}
///