WIP unravel stack index
This commit is contained in:
@@ -97,7 +97,7 @@ class _ChewieControllerHookState
|
|||||||
@override
|
@override
|
||||||
void initHook() async {
|
void initHook() async {
|
||||||
super.initHook();
|
super.initHook();
|
||||||
unawaited(_initialize());
|
_initialize().whenComplete(() => setState(() {}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -155,7 +155,6 @@ class _ChewieControllerHookState
|
|||||||
|
|
||||||
await videoPlayerController!.initialize();
|
await videoPlayerController!.initialize();
|
||||||
|
|
||||||
setState(() {
|
|
||||||
chewieController = ChewieController(
|
chewieController = ChewieController(
|
||||||
videoPlayerController: videoPlayerController!,
|
videoPlayerController: videoPlayerController!,
|
||||||
controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum,
|
controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum,
|
||||||
@@ -169,6 +168,5 @@ class _ChewieControllerHookState
|
|||||||
placeholder: hook.placeholder,
|
placeholder: hook.placeholder,
|
||||||
hideControlsTimer: hook.hideControlsTimer,
|
hideControlsTimer: hook.hideControlsTimer,
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'asset_stack.provider.g.dart';
|
||||||
|
|
||||||
class AssetStackNotifier extends StateNotifier<List<Asset>> {
|
class AssetStackNotifier extends StateNotifier<List<Asset>> {
|
||||||
final Asset _asset;
|
final Asset _asset;
|
||||||
@@ -49,3 +52,8 @@ final assetStackProvider =
|
|||||||
.sortByFileCreatedAtDesc()
|
.sortByFileCreatedAtDesc()
|
||||||
.findAll();
|
.findAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
int assetStackIndex(AssetStackIndexRef ref, Asset asset) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'asset_stack.provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$assetStackIndexHash() => r'0f2df55e929767c8c698bd432b5e6e351d000a16';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [assetStackIndex].
|
||||||
|
@ProviderFor(assetStackIndex)
|
||||||
|
const assetStackIndexProvider = AssetStackIndexFamily();
|
||||||
|
|
||||||
|
/// See also [assetStackIndex].
|
||||||
|
class AssetStackIndexFamily extends Family<int> {
|
||||||
|
/// See also [assetStackIndex].
|
||||||
|
const AssetStackIndexFamily();
|
||||||
|
|
||||||
|
/// See also [assetStackIndex].
|
||||||
|
AssetStackIndexProvider call(
|
||||||
|
Asset asset,
|
||||||
|
) {
|
||||||
|
return AssetStackIndexProvider(
|
||||||
|
asset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AssetStackIndexProvider getProviderOverride(
|
||||||
|
covariant AssetStackIndexProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
provider.asset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'assetStackIndexProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [assetStackIndex].
|
||||||
|
class AssetStackIndexProvider extends AutoDisposeProvider<int> {
|
||||||
|
/// See also [assetStackIndex].
|
||||||
|
AssetStackIndexProvider(
|
||||||
|
Asset asset,
|
||||||
|
) : this._internal(
|
||||||
|
(ref) => assetStackIndex(
|
||||||
|
ref as AssetStackIndexRef,
|
||||||
|
asset,
|
||||||
|
),
|
||||||
|
from: assetStackIndexProvider,
|
||||||
|
name: r'assetStackIndexProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$assetStackIndexHash,
|
||||||
|
dependencies: AssetStackIndexFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
AssetStackIndexFamily._allTransitiveDependencies,
|
||||||
|
asset: asset,
|
||||||
|
);
|
||||||
|
|
||||||
|
AssetStackIndexProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.asset,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final Asset asset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
int Function(AssetStackIndexRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: AssetStackIndexProvider._internal(
|
||||||
|
(ref) => create(ref as AssetStackIndexRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
asset: asset,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeProviderElement<int> createElement() {
|
||||||
|
return _AssetStackIndexProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is AssetStackIndexProvider && other.asset == asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, asset.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin AssetStackIndexRef on AutoDisposeProviderRef<int> {
|
||||||
|
/// The parameter `asset` of this provider.
|
||||||
|
Asset get asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetStackIndexProviderElement extends AutoDisposeProviderElement<int>
|
||||||
|
with AssetStackIndexRef {
|
||||||
|
_AssetStackIndexProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Asset get asset => (origin as AssetStackIndexProvider).asset;
|
||||||
|
}
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/services/asset_stack.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
|
||||||
|
class BottomGalleryBar extends ConsumerWidget {
|
||||||
|
final Asset asset;
|
||||||
|
final bool showStack;
|
||||||
|
final int stackIndex;
|
||||||
|
final PageController controller;
|
||||||
|
|
||||||
|
const BottomGalleryBar({
|
||||||
|
super.key,
|
||||||
|
required this.showStack,
|
||||||
|
required this.stackIndex,
|
||||||
|
required this.asset,
|
||||||
|
required this.controller,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId;
|
||||||
|
|
||||||
|
final stack = showStack && asset.stackChildrenCount > 0
|
||||||
|
? ref.watch(assetStackStateProvider(asset))
|
||||||
|
: <Asset>[];
|
||||||
|
final stackElements = showStack ? [asset, ...stack] : <Asset>[];
|
||||||
|
bool isParent = stackIndex == -1 || stackIndex == 0;
|
||||||
|
|
||||||
|
// !!!! itemsList and actionlist should always be in sync
|
||||||
|
final itemsList = [
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(
|
||||||
|
Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
|
||||||
|
),
|
||||||
|
label: 'control_bottom_app_bar_share'.tr(),
|
||||||
|
tooltip: 'control_bottom_app_bar_share'.tr(),
|
||||||
|
),
|
||||||
|
if (isOwner)
|
||||||
|
asset.isArchived
|
||||||
|
? BottomNavigationBarItem(
|
||||||
|
icon: const Icon(Icons.unarchive_rounded),
|
||||||
|
label: 'control_bottom_app_bar_unarchive'.tr(),
|
||||||
|
tooltip: 'control_bottom_app_bar_unarchive'.tr(),
|
||||||
|
)
|
||||||
|
: BottomNavigationBarItem(
|
||||||
|
icon: const Icon(Icons.archive_outlined),
|
||||||
|
label: 'control_bottom_app_bar_archive'.tr(),
|
||||||
|
tooltip: 'control_bottom_app_bar_archive'.tr(),
|
||||||
|
),
|
||||||
|
if (isOwner && stack.isNotEmpty)
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: const Icon(Icons.burst_mode_outlined),
|
||||||
|
label: 'control_bottom_app_bar_stack'.tr(),
|
||||||
|
tooltip: 'control_bottom_app_bar_stack'.tr(),
|
||||||
|
),
|
||||||
|
if (isOwner)
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
label: 'control_bottom_app_bar_delete'.tr(),
|
||||||
|
tooltip: 'control_bottom_app_bar_delete'.tr(),
|
||||||
|
),
|
||||||
|
if (!isOwner)
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: const Icon(Icons.download_outlined),
|
||||||
|
label: 'download'.tr(),
|
||||||
|
tooltip: 'download'.tr(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
void removeAssetFromStack() {
|
||||||
|
if (stackIndex > 0 && showStack) {
|
||||||
|
ref
|
||||||
|
.read(assetStackStateProvider(asset).notifier)
|
||||||
|
.removeChild(stackIndex - 1);
|
||||||
|
stackIndex.value = stackIndex.value - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDelete(Asset deleteAsset) async {
|
||||||
|
// Cannot delete readOnly / external assets. They are handled through library offline jobs
|
||||||
|
if (asset.isReadOnly) {
|
||||||
|
ImmichToast.show(
|
||||||
|
durationInSecond: 1,
|
||||||
|
context: context,
|
||||||
|
msg: 'asset_action_delete_err_read_only'.tr(),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Future<bool> onDelete(bool force) async {
|
||||||
|
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(
|
||||||
|
{deleteAsset},
|
||||||
|
force: force,
|
||||||
|
);
|
||||||
|
if (isDeleted && isParent) {
|
||||||
|
if (totalAssets == 1) {
|
||||||
|
// Handle only one asset
|
||||||
|
context.popRoute();
|
||||||
|
} else {
|
||||||
|
// Go to next page otherwise
|
||||||
|
controller.nextPage(
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
curve: Curves.fastLinearToSlowEaseIn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isDeleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asset is trashed
|
||||||
|
if (isTrashEnabled && !isFromTrash) {
|
||||||
|
final isDeleted = await onDelete(false);
|
||||||
|
if (isDeleted) {
|
||||||
|
// Can only trash assets stored in server. Local assets are always permanently removed for now
|
||||||
|
if (context.mounted && deleteAsset.isRemote && isParent) {
|
||||||
|
ImmichToast.show(
|
||||||
|
durationInSecond: 1,
|
||||||
|
context: context,
|
||||||
|
msg: 'Asset trashed',
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
removeAssetFromStack();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asset is permanently removed
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext _) {
|
||||||
|
return DeleteDialog(
|
||||||
|
onDelete: () async {
|
||||||
|
final isDeleted = await onDelete(true);
|
||||||
|
if (isDeleted) {
|
||||||
|
removeAssetFromStack();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showStackActionItems() {
|
||||||
|
showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
enableDrag: false,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 24.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (!isParent)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.bookmark_border_outlined,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
await ref
|
||||||
|
.read(assetStackServiceProvider)
|
||||||
|
.updateStackParent(
|
||||||
|
asset,
|
||||||
|
stackElements.elementAt(stackIndex.value),
|
||||||
|
);
|
||||||
|
ctx.pop();
|
||||||
|
context.popRoute();
|
||||||
|
},
|
||||||
|
title: const Text(
|
||||||
|
"viewer_stack_use_as_main_asset",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.copy_all_outlined,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
if (isParent) {
|
||||||
|
await ref
|
||||||
|
.read(assetStackServiceProvider)
|
||||||
|
.updateStackParent(
|
||||||
|
asset,
|
||||||
|
stackElements
|
||||||
|
.elementAt(1), // Next asset as parent
|
||||||
|
);
|
||||||
|
// Remove itself from stack
|
||||||
|
await ref.read(assetStackServiceProvider).updateStack(
|
||||||
|
stackElements.elementAt(1),
|
||||||
|
childrenToRemove: [asset],
|
||||||
|
);
|
||||||
|
ctx.pop();
|
||||||
|
context.popRoute();
|
||||||
|
} else {
|
||||||
|
await ref.read(assetStackServiceProvider).updateStack(
|
||||||
|
asset,
|
||||||
|
childrenToRemove: [
|
||||||
|
stackElements.elementAt(stackIndex.value),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
removeAssetFromStack();
|
||||||
|
ctx.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: const Text(
|
||||||
|
"viewer_remove_from_stack",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.filter_none_outlined,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
await ref.read(assetStackServiceProvider).updateStack(
|
||||||
|
asset,
|
||||||
|
childrenToRemove: stack,
|
||||||
|
);
|
||||||
|
ctx.pop();
|
||||||
|
context.popRoute();
|
||||||
|
},
|
||||||
|
title: const Text(
|
||||||
|
"viewer_unstack",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
shareAsset() {
|
||||||
|
if (asset.isOffline) {
|
||||||
|
ImmichToast.show(
|
||||||
|
durationInSecond: 1,
|
||||||
|
context: context,
|
||||||
|
msg: 'asset_action_share_err_offline'.tr(),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleArchive() {
|
||||||
|
ref.read(assetProvider.notifier).toggleArchive([asset]);
|
||||||
|
if (isParent) {
|
||||||
|
context.popRoute();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeAssetFromStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUpload(Asset asset) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext _) {
|
||||||
|
return UploadDialog(
|
||||||
|
onUpload: () {
|
||||||
|
ref
|
||||||
|
.read(manualUploadProvider.notifier)
|
||||||
|
.uploadAssets(context, [asset]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDownload() {
|
||||||
|
if (asset.isLocal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (asset.isOffline) {
|
||||||
|
ImmichToast.show(
|
||||||
|
durationInSecond: 1,
|
||||||
|
context: context,
|
||||||
|
msg: 'asset_action_share_err_offline'.tr(),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(imageViewerStateProvider.notifier).downloadAsset(
|
||||||
|
asset,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Function(int)> actionslist = [
|
||||||
|
(_) => shareAsset(),
|
||||||
|
if (isOwner) (_) => handleArchive(),
|
||||||
|
if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(),
|
||||||
|
if (isOwner) (_) => handleDelete(),
|
||||||
|
if (!isOwner) (_) => handleDownload(),
|
||||||
|
];
|
||||||
|
|
||||||
|
return IgnorePointer(
|
||||||
|
ignoring: !ref.watch(showControlsProvider),
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (stack.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 10,
|
||||||
|
bottom: 30,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: buildStackedChildren(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BottomNavigationBar(
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.4),
|
||||||
|
unselectedIconTheme: const IconThemeData(color: Colors.white),
|
||||||
|
selectedIconTheme: const IconThemeData(color: Colors.white),
|
||||||
|
unselectedLabelStyle: const TextStyle(color: Colors.black),
|
||||||
|
selectedLabelStyle: const TextStyle(color: Colors.black),
|
||||||
|
showSelectedLabels: false,
|
||||||
|
showUnselectedLabels: false,
|
||||||
|
items: itemsList,
|
||||||
|
onTap: (index) {
|
||||||
|
if (index < actionslist.length) {
|
||||||
|
actionslist[index].call(index);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
|
||||||
|
|
||||||
|
/// The video controls for the [videPlayerControlsProvider]
|
||||||
|
class VideoControls extends ConsumerWidget {
|
||||||
|
const VideoControls({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final player = ref.watch(videoPlaybackValueProvider);
|
||||||
|
print('player is $player');
|
||||||
|
final duration = player.duration;
|
||||||
|
final position = player.position;
|
||||||
|
|
||||||
|
return AnimatedOpacity(
|
||||||
|
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.4),
|
||||||
|
child: Padding(
|
||||||
|
padding: MediaQuery.of(context).orientation == Orientation.portrait
|
||||||
|
? const EdgeInsets.symmetric(horizontal: 12.0)
|
||||||
|
: const EdgeInsets.symmetric(horizontal: 64.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_formatDuration(position),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.0,
|
||||||
|
color: Colors.white.withOpacity(.75),
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Slider(
|
||||||
|
value: player.duration == Duration.zero
|
||||||
|
? 0.0
|
||||||
|
: min(
|
||||||
|
player.position.inMicroseconds /
|
||||||
|
player.duration.inMicroseconds *
|
||||||
|
100,
|
||||||
|
100,
|
||||||
|
),
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
thumbColor: Colors.white,
|
||||||
|
activeColor: Colors.white,
|
||||||
|
inactiveColor: Colors.white.withOpacity(0.75),
|
||||||
|
onChanged: (position) {
|
||||||
|
ref.read(videoPlayerControlsProvider.notifier).position =
|
||||||
|
position;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_formatDuration(duration),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.0,
|
||||||
|
color: Colors.white.withOpacity(.75),
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
ref.watch(
|
||||||
|
videoPlayerControlsProvider.select((value) => value.mute),
|
||||||
|
)
|
||||||
|
? Icons.volume_off
|
||||||
|
: Icons.volume_up,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
ref.read(videoPlayerControlsProvider.notifier).toggleMute(),
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDuration(Duration position) {
|
||||||
|
final ms = position.inMilliseconds;
|
||||||
|
|
||||||
|
int seconds = ms ~/ 1000;
|
||||||
|
final int hours = seconds ~/ 3600;
|
||||||
|
seconds = seconds % 3600;
|
||||||
|
final minutes = seconds ~/ 60;
|
||||||
|
seconds = seconds % 60;
|
||||||
|
|
||||||
|
final hoursString = hours >= 10
|
||||||
|
? '$hours'
|
||||||
|
: hours == 0
|
||||||
|
? '00'
|
||||||
|
: '0$hours';
|
||||||
|
|
||||||
|
final minutesString = minutes >= 10
|
||||||
|
? '$minutes'
|
||||||
|
: minutes == 0
|
||||||
|
? '00'
|
||||||
|
: '0$minutes';
|
||||||
|
|
||||||
|
final secondsString = seconds >= 10
|
||||||
|
? '$seconds'
|
||||||
|
: seconds == 0
|
||||||
|
? '00'
|
||||||
|
: '0$seconds';
|
||||||
|
|
||||||
|
final formattedTime =
|
||||||
|
'${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString';
|
||||||
|
|
||||||
|
return formattedTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,12 +15,11 @@ import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart';
|
import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/asset_viewer/services/asset_stack.service.dart';
|
import 'package:immich_mobile/modules/asset_viewer/services/asset_stack.service.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/advanced_bottom_sheet.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/advanced_bottom_sheet.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/ui/bottom_gallery_bar.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||||
@@ -94,16 +93,15 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
final isFromDto = currentAsset.id == Isar.autoIncrement;
|
final isFromDto = currentAsset.id == Isar.autoIncrement;
|
||||||
final album = ref.watch(currentAlbumProvider);
|
final album = ref.watch(currentAlbumProvider);
|
||||||
|
|
||||||
Asset asset() => stackIndex.value == -1
|
Asset asset = stackIndex.value == -1
|
||||||
? currentAsset
|
? currentAsset
|
||||||
: stackElements.elementAt(stackIndex.value);
|
: stackElements.elementAt(stackIndex.value);
|
||||||
final isOwner = asset().ownerId == ref.watch(currentUserProvider)?.isarId;
|
final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId;
|
||||||
final isPartner = ref
|
final isPartner = ref
|
||||||
.watch(partnerSharedWithProvider)
|
.watch(partnerSharedWithProvider)
|
||||||
.map((e) => e.isarId)
|
.map((e) => e.isarId)
|
||||||
.contains(asset().ownerId);
|
.contains(asset.ownerId);
|
||||||
|
|
||||||
bool isParent = stackIndex.value == -1 || stackIndex.value == 0;
|
|
||||||
|
|
||||||
// Listen provider to prevent autoDispose when navigating to other routes from within the gallery page
|
// Listen provider to prevent autoDispose when navigating to other routes from within the gallery page
|
||||||
ref.listen(currentAssetProvider, (_, __) {});
|
ref.listen(currentAssetProvider, (_, __) {});
|
||||||
@@ -111,11 +109,11 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
() {
|
() {
|
||||||
// Delay state update to after the execution of build method
|
// Delay state update to after the execution of build method
|
||||||
Future.microtask(
|
Future.microtask(
|
||||||
() => ref.read(currentAssetProvider.notifier).set(asset()),
|
() => ref.read(currentAssetProvider.notifier).set(asset),
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[asset()],
|
[asset],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@@ -168,82 +166,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
child: ref
|
child: ref
|
||||||
.watch(appSettingsServiceProvider)
|
.watch(appSettingsServiceProvider)
|
||||||
.getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)
|
.getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)
|
||||||
? AdvancedBottomSheet(assetDetail: asset())
|
? AdvancedBottomSheet(assetDetail: asset)
|
||||||
: ExifBottomSheet(asset: asset()),
|
: ExifBottomSheet(asset: asset),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeAssetFromStack() {
|
|
||||||
if (stackIndex.value > 0 && showStack) {
|
|
||||||
ref
|
|
||||||
.read(assetStackStateProvider(currentAsset).notifier)
|
|
||||||
.removeChild(stackIndex.value - 1);
|
|
||||||
stackIndex.value = stackIndex.value - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleDelete(Asset deleteAsset) async {
|
|
||||||
// Cannot delete readOnly / external assets. They are handled through library offline jobs
|
|
||||||
if (asset().isReadOnly) {
|
|
||||||
ImmichToast.show(
|
|
||||||
durationInSecond: 1,
|
|
||||||
context: context,
|
|
||||||
msg: 'asset_action_delete_err_read_only'.tr(),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Future<bool> onDelete(bool force) async {
|
|
||||||
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(
|
|
||||||
{deleteAsset},
|
|
||||||
force: force,
|
|
||||||
);
|
|
||||||
if (isDeleted && isParent) {
|
|
||||||
if (totalAssets == 1) {
|
|
||||||
// Handle only one asset
|
|
||||||
context.popRoute();
|
|
||||||
} else {
|
|
||||||
// Go to next page otherwise
|
|
||||||
controller.nextPage(
|
|
||||||
duration: const Duration(milliseconds: 100),
|
|
||||||
curve: Curves.fastLinearToSlowEaseIn,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isDeleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asset is trashed
|
|
||||||
if (isTrashEnabled && !isFromTrash) {
|
|
||||||
final isDeleted = await onDelete(false);
|
|
||||||
if (isDeleted) {
|
|
||||||
// Can only trash assets stored in server. Local assets are always permanently removed for now
|
|
||||||
if (context.mounted && deleteAsset.isRemote && isParent) {
|
|
||||||
ImmichToast.show(
|
|
||||||
durationInSecond: 1,
|
|
||||||
context: context,
|
|
||||||
msg: 'Asset trashed',
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
removeAssetFromStack();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asset is permanently removed
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext _) {
|
|
||||||
return DeleteDialog(
|
|
||||||
onDelete: () async {
|
|
||||||
final isDeleted = await onDelete(true);
|
|
||||||
if (isDeleted) {
|
|
||||||
removeAssetFromStack();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -293,63 +217,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shareAsset() {
|
|
||||||
if (asset().isOffline) {
|
|
||||||
ImmichToast.show(
|
|
||||||
durationInSecond: 1,
|
|
||||||
context: context,
|
|
||||||
msg: 'asset_action_share_err_offline'.tr(),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ref.read(imageViewerStateProvider.notifier).shareAsset(asset(), context);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleArchive(Asset asset) {
|
|
||||||
ref.read(assetProvider.notifier).toggleArchive([asset]);
|
|
||||||
if (isParent) {
|
|
||||||
context.popRoute();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
removeAssetFromStack();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUpload(Asset asset) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext _) {
|
|
||||||
return UploadDialog(
|
|
||||||
onUpload: () {
|
|
||||||
ref
|
|
||||||
.read(manualUploadProvider.notifier)
|
|
||||||
.uploadAssets(context, [asset]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDownload() {
|
|
||||||
if (asset().isLocal) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (asset().isOffline) {
|
|
||||||
ImmichToast.show(
|
|
||||||
durationInSecond: 1,
|
|
||||||
context: context,
|
|
||||||
msg: 'asset_action_share_err_offline'.tr(),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.read(imageViewerStateProvider.notifier).downloadAsset(
|
|
||||||
asset(),
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleActivities() {
|
handleActivities() {
|
||||||
if (album != null && album.shared && album.remoteId != null) {
|
if (album != null && album.shared && album.remoteId != null) {
|
||||||
context.pushRoute(const ActivitiesRoute());
|
context.pushRoute(const ActivitiesRoute());
|
||||||
@@ -368,22 +235,22 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
isOwner: isOwner,
|
isOwner: isOwner,
|
||||||
isPartner: isPartner,
|
isPartner: isPartner,
|
||||||
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
||||||
asset: asset(),
|
asset: asset,
|
||||||
onMoreInfoPressed: showInfo,
|
onMoreInfoPressed: showInfo,
|
||||||
onFavorite: toggleFavorite,
|
onFavorite: toggleFavorite,
|
||||||
onUploadPressed:
|
onUploadPressed:
|
||||||
asset().isLocal ? () => handleUpload(asset()) : null,
|
asset.isLocal ? () => handleUpload(asset) : null,
|
||||||
onDownloadPressed: asset().isLocal
|
onDownloadPressed: asset.isLocal
|
||||||
? null
|
? null
|
||||||
: () =>
|
: () =>
|
||||||
ref.read(imageViewerStateProvider.notifier).downloadAsset(
|
ref.read(imageViewerStateProvider.notifier).downloadAsset(
|
||||||
asset(),
|
asset,
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
onToggleMotionVideo: (() {
|
onToggleMotionVideo: (() {
|
||||||
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
||||||
}),
|
}),
|
||||||
onAddToAlbumPressed: () => addToAlbum(asset()),
|
onAddToAlbumPressed: () => addToAlbum(asset),
|
||||||
onActivitiesPressed: handleActivities,
|
onActivitiesPressed: handleActivities,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -391,71 +258,44 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildProgressBar() {
|
// TODO: Migrate to a custom bottom bar and handle long press to delete
|
||||||
final playerValue = ref.watch(videoPlaybackValueProvider);
|
Widget buildBottomBar() {
|
||||||
|
}
|
||||||
|
|
||||||
return Expanded(
|
useEffect(
|
||||||
child: Slider(
|
() {
|
||||||
value: playerValue.duration == Duration.zero
|
if (ref.read(showControlsProvider)) {
|
||||||
? 0.0
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
: min(
|
} else {
|
||||||
playerValue.position.inMicroseconds /
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
playerValue.duration.inMicroseconds *
|
}
|
||||||
100,
|
return null;
|
||||||
100,
|
|
||||||
),
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
thumbColor: Colors.white,
|
|
||||||
activeColor: Colors.white,
|
|
||||||
inactiveColor: Colors.white.withOpacity(0.75),
|
|
||||||
onChanged: (position) {
|
|
||||||
ref.read(videoPlayerControlsProvider.notifier).position = position;
|
|
||||||
},
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
// No need to await this
|
||||||
|
unawaited(
|
||||||
|
// Delay this a bit so we can finish loading the page
|
||||||
|
Future.delayed(const Duration(milliseconds: 400)).then(
|
||||||
|
// Precache the next image
|
||||||
|
(_) => precacheNextImage(currentIndex.value + 1),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
return null;
|
||||||
|
},
|
||||||
Text buildPosition() {
|
[],
|
||||||
final position = ref
|
|
||||||
.watch(videoPlaybackValueProvider.select((value) => value.position));
|
|
||||||
|
|
||||||
return Text(
|
|
||||||
_formatDuration(position),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14.0,
|
|
||||||
color: Colors.white.withOpacity(.75),
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Text buildDuration() {
|
ref.listen(showControlsProvider, (_, show) {
|
||||||
final duration = ref
|
if (show) {
|
||||||
.watch(videoPlaybackValueProvider.select((value) => value.duration));
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
} else {
|
||||||
return Text(
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
_formatDuration(duration),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14.0,
|
|
||||||
color: Colors.white.withOpacity(.75),
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildMuteButton() {
|
|
||||||
return IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
ref.watch(videoPlayerControlsProvider.select((value) => value.mute))
|
|
||||||
? Icons.volume_off
|
|
||||||
: Icons.volume_up,
|
|
||||||
),
|
|
||||||
onPressed: () =>
|
|
||||||
ref.read(videoPlayerControlsProvider.notifier).toggleMute(),
|
|
||||||
color: Colors.white,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Widget buildStackedChildren() {
|
Widget buildStackedChildren() {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
@@ -495,246 +335,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showStackActionItems() {
|
|
||||||
showModalBottomSheet<void>(
|
|
||||||
context: context,
|
|
||||||
enableDrag: false,
|
|
||||||
builder: (BuildContext ctx) {
|
|
||||||
return SafeArea(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 24.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (!isParent)
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(
|
|
||||||
Icons.bookmark_border_outlined,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
onTap: () async {
|
|
||||||
await ref
|
|
||||||
.read(assetStackServiceProvider)
|
|
||||||
.updateStackParent(
|
|
||||||
currentAsset,
|
|
||||||
stackElements.elementAt(stackIndex.value),
|
|
||||||
);
|
|
||||||
ctx.pop();
|
|
||||||
context.popRoute();
|
|
||||||
},
|
|
||||||
title: const Text(
|
|
||||||
"viewer_stack_use_as_main_asset",
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(
|
|
||||||
Icons.copy_all_outlined,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
onTap: () async {
|
|
||||||
if (isParent) {
|
|
||||||
await ref
|
|
||||||
.read(assetStackServiceProvider)
|
|
||||||
.updateStackParent(
|
|
||||||
currentAsset,
|
|
||||||
stackElements
|
|
||||||
.elementAt(1), // Next asset as parent
|
|
||||||
);
|
|
||||||
// Remove itself from stack
|
|
||||||
await ref.read(assetStackServiceProvider).updateStack(
|
|
||||||
stackElements.elementAt(1),
|
|
||||||
childrenToRemove: [currentAsset],
|
|
||||||
);
|
|
||||||
ctx.pop();
|
|
||||||
context.popRoute();
|
|
||||||
} else {
|
|
||||||
await ref.read(assetStackServiceProvider).updateStack(
|
|
||||||
currentAsset,
|
|
||||||
childrenToRemove: [
|
|
||||||
stackElements.elementAt(stackIndex.value),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
removeAssetFromStack();
|
|
||||||
ctx.pop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title: const Text(
|
|
||||||
"viewer_remove_from_stack",
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(
|
|
||||||
Icons.filter_none_outlined,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
onTap: () async {
|
|
||||||
await ref.read(assetStackServiceProvider).updateStack(
|
|
||||||
currentAsset,
|
|
||||||
childrenToRemove: stack,
|
|
||||||
);
|
|
||||||
ctx.pop();
|
|
||||||
context.popRoute();
|
|
||||||
},
|
|
||||||
title: const Text(
|
|
||||||
"viewer_unstack",
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Migrate to a custom bottom bar and handle long press to delete
|
|
||||||
Widget buildBottomBar() {
|
|
||||||
// !!!! itemsList and actionlist should always be in sync
|
|
||||||
final itemsList = [
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: Icon(
|
|
||||||
Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
|
|
||||||
),
|
|
||||||
label: 'control_bottom_app_bar_share'.tr(),
|
|
||||||
tooltip: 'control_bottom_app_bar_share'.tr(),
|
|
||||||
),
|
|
||||||
if (isOwner)
|
|
||||||
asset().isArchived
|
|
||||||
? BottomNavigationBarItem(
|
|
||||||
icon: const Icon(Icons.unarchive_rounded),
|
|
||||||
label: 'control_bottom_app_bar_unarchive'.tr(),
|
|
||||||
tooltip: 'control_bottom_app_bar_unarchive'.tr(),
|
|
||||||
)
|
|
||||||
: BottomNavigationBarItem(
|
|
||||||
icon: const Icon(Icons.archive_outlined),
|
|
||||||
label: 'control_bottom_app_bar_archive'.tr(),
|
|
||||||
tooltip: 'control_bottom_app_bar_archive'.tr(),
|
|
||||||
),
|
|
||||||
if (isOwner && stack.isNotEmpty)
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: const Icon(Icons.burst_mode_outlined),
|
|
||||||
label: 'control_bottom_app_bar_stack'.tr(),
|
|
||||||
tooltip: 'control_bottom_app_bar_stack'.tr(),
|
|
||||||
),
|
|
||||||
if (isOwner)
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: const Icon(Icons.delete_outline),
|
|
||||||
label: 'control_bottom_app_bar_delete'.tr(),
|
|
||||||
tooltip: 'control_bottom_app_bar_delete'.tr(),
|
|
||||||
),
|
|
||||||
if (!isOwner)
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: const Icon(Icons.download_outlined),
|
|
||||||
label: 'download'.tr(),
|
|
||||||
tooltip: 'download'.tr(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
List<Function(int)> actionslist = [
|
|
||||||
(_) => shareAsset(),
|
|
||||||
if (isOwner) (_) => handleArchive(asset()),
|
|
||||||
if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(),
|
|
||||||
if (isOwner) (_) => handleDelete(asset()),
|
|
||||||
if (!isOwner) (_) => handleDownload(),
|
|
||||||
];
|
|
||||||
|
|
||||||
return IgnorePointer(
|
|
||||||
ignoring: !ref.watch(showControlsProvider),
|
|
||||||
child: AnimatedOpacity(
|
|
||||||
duration: const Duration(milliseconds: 100),
|
|
||||||
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
if (stack.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 10,
|
|
||||||
bottom: 30,
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 40,
|
|
||||||
child: buildStackedChildren(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Visibility(
|
|
||||||
visible: !asset().isImage && !isPlayingMotionVideo.value,
|
|
||||||
child: Container(
|
|
||||||
color: Colors.black.withOpacity(0.4),
|
|
||||||
child: Padding(
|
|
||||||
padding: MediaQuery.of(context).orientation ==
|
|
||||||
Orientation.portrait
|
|
||||||
? const EdgeInsets.symmetric(horizontal: 12.0)
|
|
||||||
: const EdgeInsets.symmetric(horizontal: 64.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
buildPosition(),
|
|
||||||
buildProgressBar(),
|
|
||||||
buildDuration(),
|
|
||||||
buildMuteButton(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BottomNavigationBar(
|
|
||||||
backgroundColor: Colors.black.withOpacity(0.4),
|
|
||||||
unselectedIconTheme: const IconThemeData(color: Colors.white),
|
|
||||||
selectedIconTheme: const IconThemeData(color: Colors.white),
|
|
||||||
unselectedLabelStyle: const TextStyle(color: Colors.black),
|
|
||||||
selectedLabelStyle: const TextStyle(color: Colors.black),
|
|
||||||
showSelectedLabels: false,
|
|
||||||
showUnselectedLabels: false,
|
|
||||||
items: itemsList,
|
|
||||||
onTap: (index) {
|
|
||||||
if (index < actionslist.length) {
|
|
||||||
actionslist[index].call(index);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() {
|
|
||||||
if (ref.read(showControlsProvider)) {
|
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
|
||||||
} else {
|
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() {
|
|
||||||
// No need to await this
|
|
||||||
unawaited(
|
|
||||||
// Delay this a bit so we can finish loading the page
|
|
||||||
Future.delayed(const Duration(milliseconds: 400)).then(
|
|
||||||
// Precache the next image
|
|
||||||
(_) => precacheNextImage(currentIndex.value + 1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
ref.listen(showControlsProvider, (_, show) {
|
|
||||||
if (show) {
|
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
|
||||||
} else {
|
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: false,
|
canPop: false,
|
||||||
onPopInvoked: (_) {
|
onPopInvoked: (_) {
|
||||||
@@ -762,7 +362,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
ImmichThumbnail(
|
ImmichThumbnail(
|
||||||
asset: asset(),
|
asset: asset,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -790,7 +390,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
builder: (context, index) {
|
builder: (context, index) {
|
||||||
final a =
|
final a =
|
||||||
index == currentIndex.value ? asset() : loadAsset(index);
|
index == currentIndex.value ? asset : loadAsset(index);
|
||||||
final ImageProvider provider =
|
final ImageProvider provider =
|
||||||
ImmichImage.imageProvider(asset: a);
|
ImmichImage.imageProvider(asset: a);
|
||||||
|
|
||||||
@@ -870,44 +470,15 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: buildBottomBar(),
|
child: Column(
|
||||||
|
children: [
|
||||||
|
buildStackedChildren(),
|
||||||
|
BottomGalleryBar(
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDuration(Duration position) {
|
|
||||||
final ms = position.inMilliseconds;
|
|
||||||
|
|
||||||
int seconds = ms ~/ 1000;
|
|
||||||
final int hours = seconds ~/ 3600;
|
|
||||||
seconds = seconds % 3600;
|
|
||||||
final minutes = seconds ~/ 60;
|
|
||||||
seconds = seconds % 60;
|
|
||||||
|
|
||||||
final hoursString = hours >= 10
|
|
||||||
? '$hours'
|
|
||||||
: hours == 0
|
|
||||||
? '00'
|
|
||||||
: '0$hours';
|
|
||||||
|
|
||||||
final minutesString = minutes >= 10
|
|
||||||
? '$minutes'
|
|
||||||
: minutes == 0
|
|
||||||
? '00'
|
|
||||||
: '0$minutes';
|
|
||||||
|
|
||||||
final secondsString = seconds >= 10
|
|
||||||
? '$seconds'
|
|
||||||
: seconds == 0
|
|
||||||
? '00'
|
|
||||||
: '0$seconds';
|
|
||||||
|
|
||||||
final formattedTime =
|
|
||||||
'${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString';
|
|
||||||
|
|
||||||
return formattedTime;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/hooks/chewiew_controller_hook.dart';
|
import 'package:immich_mobile/modules/asset_viewer/hooks/chewiew_controller_hook.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/ui/video_controls.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
|
||||||
@@ -50,13 +51,16 @@ class VideoViewerPage extends HookWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Loading
|
// Loading
|
||||||
|
final size = MediaQuery.of(context).size;
|
||||||
|
print('showControls $showControls and isMotion $isMotionVideo');
|
||||||
return PopScope(
|
return PopScope(
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 400),
|
duration: const Duration(milliseconds: 400),
|
||||||
child: Builder(
|
child: Stack(
|
||||||
builder: (context) {
|
children: [
|
||||||
if (controller == null) {
|
Visibility(
|
||||||
return Stack(
|
visible: controller == null,
|
||||||
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
if (placeholder != null) placeholder!,
|
if (placeholder != null) placeholder!,
|
||||||
const Positioned.fill(
|
const Positioned.fill(
|
||||||
@@ -67,18 +71,30 @@ class VideoViewerPage extends HookWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
}
|
),
|
||||||
|
if (controller != null)
|
||||||
final size = MediaQuery.of(context).size;
|
SizedBox(
|
||||||
return SizedBox(
|
|
||||||
height: size.height,
|
height: size.height,
|
||||||
width: size.width,
|
width: size.width,
|
||||||
child: Chewie(
|
child: Chewie(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
if (controller != null)
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: SizedBox(
|
||||||
|
width: size.width,
|
||||||
|
child: Visibility(
|
||||||
|
visible: true,
|
||||||
|
child: const VideoControls(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ part of 'map_state.provider.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$mapStateNotifierHash() => r'3b509b57b7400b09817e9caee9debf899172cd52';
|
String _$mapStateNotifierHash() => r'6408d616ec9fc0d1ff26e25692417c43504ff754';
|
||||||
|
|
||||||
/// See also [MapStateNotifier].
|
/// See also [MapStateNotifier].
|
||||||
@ProviderFor(MapStateNotifier)
|
@ProviderFor(MapStateNotifier)
|
||||||
|
|||||||
@@ -1393,7 +1393,7 @@ class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
|||||||
void Function()? onPaused,
|
void Function()? onPaused,
|
||||||
Widget? placeholder,
|
Widget? placeholder,
|
||||||
bool showControls = true,
|
bool showControls = true,
|
||||||
Duration hideControlsTimer = const Duration(milliseconds: 1500),
|
Duration hideControlsTimer = const Duration(seconds: 5),
|
||||||
bool showDownloadingIndicator = true,
|
bool showDownloadingIndicator = true,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
@@ -1429,7 +1429,7 @@ class VideoViewerRouteArgs {
|
|||||||
this.onPaused,
|
this.onPaused,
|
||||||
this.placeholder,
|
this.placeholder,
|
||||||
this.showControls = true,
|
this.showControls = true,
|
||||||
this.hideControlsTimer = const Duration(milliseconds: 1500),
|
this.hideControlsTimer = const Duration(seconds: 5),
|
||||||
this.showDownloadingIndicator = true,
|
this.showDownloadingIndicator = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user