Merge branch 'main' into edit-date-time-action

This commit is contained in:
Daimolean
2025-07-24 02:12:56 +08:00
committed by GitHub
73 changed files with 2252 additions and 1341 deletions

View File

@@ -1,16 +1,50 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class UploadActionButton extends ConsumerWidget {
const UploadActionButton({super.key});
final ActionSource source;
const UploadActionButton({super.key, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final result = await ref.read(actionProvider.notifier).upload(source);
final successMessage = 'upload_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
if (context.mounted) {
ImmichToast.show(
context: context,
msg: result.success
? successMessage
: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: result.success ? ToastType.success : ToastType.error,
);
ref.read(multiSelectProvider.notifier).reset();
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton(
iconData: Icons.backup_outlined,
label: "upload".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@@ -6,11 +6,7 @@ class StackChildrenNotifier
extends AutoDisposeFamilyAsyncNotifier<List<RemoteAsset>, BaseAsset?> {
@override
Future<List<RemoteAsset>> build(BaseAsset? asset) async {
if (asset == null ||
asset is! RemoteAsset ||
asset.stackId == null ||
// The stackCount check is to ensure we only fetch stacks for timelines that have stacks
asset.stackCount == 0) {
if (asset == null || asset is! RemoteAsset || asset.stackId == null) {
return const [];
}

View File

@@ -5,6 +5,7 @@ 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/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@@ -37,6 +38,8 @@ class ViewerBottomBar extends ConsumerWidget {
final actions = <Widget>[
const ShareActionButton(source: ActionSource.viewer),
if (asset.isLocalOnly)
const UploadActionButton(source: ActionSource.viewer),
if (asset.hasRemote && isOwner)
const ArchiveActionButton(source: ActionSource.viewer),
];
@@ -50,31 +53,30 @@ class ViewerBottomBar extends ConsumerWidget {
duration: Durations.short4,
child: isSheetOpen
? const SizedBox.shrink()
: SafeArea(
child: Theme(
data: context.themeData.copyWith(
iconTheme:
const IconThemeData(size: 22, color: Colors.white),
textTheme: context.themeData.textTheme.copyWith(
labelLarge:
context.themeData.textTheme.labelLarge?.copyWith(
color: Colors.white,
),
: Theme(
data: context.themeData.copyWith(
iconTheme:
const IconThemeData(size: 22, color: Colors.white),
textTheme: context.themeData.textTheme.copyWith(
labelLarge:
context.themeData.textTheme.labelLarge?.copyWith(
color: Colors.white,
),
),
child: Container(
height: asset.isVideo ? 160 : 80,
color: Colors.black.withAlpha(125),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (asset.isVideo) const VideoControls(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: actions,
),
],
),
),
child: Container(
height: context.padding.bottom + (asset.isVideo ? 160 : 80),
color: Colors.black.withAlpha(125),
padding: EdgeInsets.only(bottom: context.padding.bottom),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (asset.isVideo) const VideoControls(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: actions,
),
],
),
),
),

View File

@@ -63,7 +63,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
],
if (asset.storage == AssetState.local) ...[
const DeleteLocalActionButton(source: ActionSource.viewer),
const UploadActionButton(),
const UploadActionButton(source: ActionSource.timeline),
],
];

View File

@@ -27,6 +27,26 @@ import 'package:logging/logging.dart';
import 'package:native_video_player/native_video_player.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
bool _isCurrentAsset(
BaseAsset asset,
BaseAsset? currentAsset,
) {
if (asset is RemoteAsset) {
return switch (currentAsset) {
RemoteAsset remoteAsset => remoteAsset.id == asset.id,
LocalAsset localAsset => localAsset.remoteId == asset.id,
_ => false,
};
} else if (asset is LocalAsset) {
return switch (currentAsset) {
RemoteAsset remoteAsset => remoteAsset.localId == asset.id,
LocalAsset localAsset => localAsset.id == asset.id,
_ => false,
};
}
return false;
}
class NativeVideoViewer extends HookConsumerWidget {
final BaseAsset asset;
final bool showControls;
@@ -56,7 +76,7 @@ class NativeVideoViewer extends HookConsumerWidget {
// If the swipe is completed, `isCurrent` will be true for video B after a delay.
// If the swipe is canceled, `currentAsset` will not have changed and video A will continue to play.
final currentAsset = useState(ref.read(currentAssetNotifier));
final isCurrent = currentAsset.value == asset;
final isCurrent = _isCurrentAsset(asset, currentAsset.value);
// Used to show the placeholder during hero animations for remote videos to avoid a stutter
final isVisible = useState(Platform.isIOS && asset.hasLocal);

View File

@@ -53,7 +53,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),
const UploadActionButton(),
const UploadActionButton(source: ActionSource.timeline),
],
],
);

View File

@@ -53,7 +53,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),
const UploadActionButton(),
const UploadActionButton(source: ActionSource.timeline),
],
],
);

View File

@@ -56,7 +56,7 @@ class GeneralBottomSheet extends ConsumerWidget {
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),
const UploadActionButton(),
const UploadActionButton(source: ActionSource.timeline),
],
],
);

View File

@@ -18,7 +18,7 @@ class LocalAlbumBottomSheet extends ConsumerWidget {
actions: [
ShareActionButton(source: ActionSource.timeline),
DeleteLocalActionButton(source: ActionSource.timeline),
UploadActionButton(),
UploadActionButton(source: ActionSource.timeline),
],
);
}

View File

@@ -56,7 +56,7 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),
const UploadActionButton(),
const UploadActionButton(source: ActionSource.timeline),
],
RemoveFromAlbumActionButton(
source: ActionSource.timeline,

View File

@@ -54,7 +54,7 @@ class ThumbnailTile extends ConsumerWidget {
: const BoxDecoration();
final hasStack =
asset is RemoteAsset && (asset as RemoteAsset).stackCount > 0;
asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
return Stack(
children: [
@@ -86,9 +86,7 @@ class ThumbnailTile extends ConsumerWidget {
right: 10.0,
top: asset.isVideo ? 24.0 : 6.0,
),
child: _StackIndicator(
stackCount: (asset as RemoteAsset).stackCount,
),
child: const _TileOverlayIcon(Icons.burst_mode_rounded),
),
),
if (asset.isVideo)
@@ -198,40 +196,6 @@ class _SelectionIndicator extends StatelessWidget {
}
}
class _StackIndicator extends StatelessWidget {
final int stackCount;
const _StackIndicator({required this.stackCount});
@override
Widget build(BuildContext context) {
return Row(
spacing: 3,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
// CrossAxisAlignment.start looks more centered vertically than CrossAxisAlignment.center
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stackCount.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
shadows: [
Shadow(
blurRadius: 5.0,
color: Color.fromRGBO(0, 0, 0, 0.6),
),
],
),
),
const _TileOverlayIcon(Icons.burst_mode_rounded),
],
);
}
}
class _VideoIndicator extends StatelessWidget {
final Duration duration;
const _VideoIndicator(this.duration);