|
@@ -1,4 +1,5 @@
|
|
import 'dart:io';
|
|
import 'dart:io';
|
|
|
|
+import 'dart:math';
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
import 'package:auto_route/auto_route.dart';
|
|
import 'package:auto_route/auto_route.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
@@ -6,8 +7,11 @@ import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
|
import 'package:hooks_riverpod/hooks_riverpod.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/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/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/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';
|
|
@@ -49,9 +53,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
|
|
final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
|
|
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
|
|
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
|
|
final isZoomed = useState<bool>(false);
|
|
final isZoomed = useState<bool>(false);
|
|
- final showAppBar = useState<bool>(true);
|
|
|
|
final isPlayingMotionVideo = useState(false);
|
|
final isPlayingMotionVideo = useState(false);
|
|
final isPlayingVideo = useState(false);
|
|
final isPlayingVideo = useState(false);
|
|
|
|
+ final progressValue = useState(0.0);
|
|
Offset? localPosition;
|
|
Offset? localPosition;
|
|
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
|
|
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
|
|
final currentIndex = useState(initialIndex);
|
|
final currentIndex = useState(initialIndex);
|
|
@@ -60,15 +64,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
|
|
|
|
Asset asset() => watchedAsset.value ?? currentAsset;
|
|
Asset asset() => watchedAsset.value ?? currentAsset;
|
|
|
|
|
|
- showAppBar.addListener(() {
|
|
|
|
- // Change to and from immersive mode, hiding navigation and app bar
|
|
|
|
- if (showAppBar.value) {
|
|
|
|
- SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
|
|
|
- } else {
|
|
|
|
- SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
useEffect(
|
|
useEffect(
|
|
() {
|
|
() {
|
|
isLoadPreview.value =
|
|
isLoadPreview.value =
|
|
@@ -277,15 +272,11 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
}
|
|
}
|
|
|
|
|
|
buildAppBar() {
|
|
buildAppBar() {
|
|
- final show = (showAppBar.value || // onTap has the final say
|
|
|
|
- (showAppBar.value && !isZoomed.value)) &&
|
|
|
|
- !isPlayingVideo.value;
|
|
|
|
-
|
|
|
|
return IgnorePointer(
|
|
return IgnorePointer(
|
|
- ignoring: !show,
|
|
|
|
|
|
+ ignoring: !ref.watch(showControlsProvider),
|
|
child: AnimatedOpacity(
|
|
child: AnimatedOpacity(
|
|
duration: const Duration(milliseconds: 100),
|
|
duration: const Duration(milliseconds: 100),
|
|
- opacity: show ? 1.0 : 0.0,
|
|
|
|
|
|
+ opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
|
|
child: Container(
|
|
child: Container(
|
|
color: Colors.black.withOpacity(0.4),
|
|
color: Colors.black.withOpacity(0.4),
|
|
child: TopControlAppBar(
|
|
child: TopControlAppBar(
|
|
@@ -313,65 +304,160 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
- buildBottomBar() {
|
|
|
|
- final show = (showAppBar.value || // onTap has the final say
|
|
|
|
- (showAppBar.value && !isZoomed.value)) &&
|
|
|
|
- !isPlayingVideo.value;
|
|
|
|
|
|
+ Widget buildProgressBar() {
|
|
|
|
+ final playerValue = ref.watch(videoPlaybackValueProvider);
|
|
|
|
+
|
|
|
|
+ return Expanded(
|
|
|
|
+ child: Slider(
|
|
|
|
+ value: playerValue.duration == Duration.zero
|
|
|
|
+ ? 0.0
|
|
|
|
+ : min(
|
|
|
|
+ playerValue.position.inMicroseconds /
|
|
|
|
+ playerValue.duration.inMicroseconds *
|
|
|
|
+ 100,
|
|
|
|
+ 100,
|
|
|
|
+ ),
|
|
|
|
+ min: 0,
|
|
|
|
+ max: 100,
|
|
|
|
+ thumbColor: Colors.white,
|
|
|
|
+ activeColor: Colors.white,
|
|
|
|
+ inactiveColor: Colors.white.withOpacity(0.75),
|
|
|
|
+ onChanged: (position) {
|
|
|
|
+ progressValue.value = position;
|
|
|
|
+ ref.read(videoPlayerControlsProvider.notifier).position = position;
|
|
|
|
+ },
|
|
|
|
+ ),
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ 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() {
|
|
|
|
+ final duration = ref
|
|
|
|
+ .watch(videoPlaybackValueProvider.select((value) => value.duration));
|
|
|
|
+
|
|
|
|
+ return Text(
|
|
|
|
+ _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,
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ buildBottomBar() {
|
|
return IgnorePointer(
|
|
return IgnorePointer(
|
|
- ignoring: !show,
|
|
|
|
|
|
+ ignoring: !ref.watch(showControlsProvider),
|
|
child: AnimatedOpacity(
|
|
child: AnimatedOpacity(
|
|
duration: const Duration(milliseconds: 100),
|
|
duration: const Duration(milliseconds: 100),
|
|
- opacity: show ? 1.0 : 0.0,
|
|
|
|
- child: 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: [
|
|
|
|
- BottomNavigationBarItem(
|
|
|
|
- icon: const Icon(Icons.ios_share_rounded),
|
|
|
|
- label: 'control_bottom_app_bar_share'.tr(),
|
|
|
|
- tooltip: 'control_bottom_app_bar_share'.tr(),
|
|
|
|
- ),
|
|
|
|
- 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(),
|
|
|
|
|
|
+ opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
|
|
|
|
+ child: Column(
|
|
|
|
+ children: [
|
|
|
|
+ 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(),
|
|
|
|
+ ],
|
|
),
|
|
),
|
|
- BottomNavigationBarItem(
|
|
|
|
- icon: const Icon(Icons.delete_outline),
|
|
|
|
- label: 'control_bottom_app_bar_delete'.tr(),
|
|
|
|
- tooltip: 'control_bottom_app_bar_delete'.tr(),
|
|
|
|
|
|
+ ),
|
|
|
|
+ ),
|
|
|
|
+ ),
|
|
|
|
+ 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: [
|
|
|
|
+ BottomNavigationBarItem(
|
|
|
|
+ icon: const Icon(Icons.ios_share_rounded),
|
|
|
|
+ label: 'control_bottom_app_bar_share'.tr(),
|
|
|
|
+ tooltip: 'control_bottom_app_bar_share'.tr(),
|
|
|
|
+ ),
|
|
|
|
+ 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(),
|
|
|
|
+ ),
|
|
|
|
+ BottomNavigationBarItem(
|
|
|
|
+ icon: const Icon(Icons.delete_outline),
|
|
|
|
+ label: 'control_bottom_app_bar_delete'.tr(),
|
|
|
|
+ tooltip: 'control_bottom_app_bar_delete'.tr(),
|
|
|
|
+ ),
|
|
|
|
+ ],
|
|
|
|
+ onTap: (index) {
|
|
|
|
+ switch (index) {
|
|
|
|
+ case 0:
|
|
|
|
+ shareAsset();
|
|
|
|
+ break;
|
|
|
|
+ case 1:
|
|
|
|
+ handleArchive(asset());
|
|
|
|
+ break;
|
|
|
|
+ case 2:
|
|
|
|
+ handleDelete(asset());
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ },
|
|
),
|
|
),
|
|
],
|
|
],
|
|
- onTap: (index) {
|
|
|
|
- switch (index) {
|
|
|
|
- case 0:
|
|
|
|
- shareAsset();
|
|
|
|
- break;
|
|
|
|
- case 1:
|
|
|
|
- handleArchive(asset());
|
|
|
|
- break;
|
|
|
|
- case 2:
|
|
|
|
- handleDelete(asset());
|
|
|
|
- break;
|
|
|
|
- }
|
|
|
|
- },
|
|
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ ref.listen(showControlsProvider, (_, show) {
|
|
|
|
+ if (show) {
|
|
|
|
+ SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
|
|
|
+ } else {
|
|
|
|
+ SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
ImageProvider imageProvider(Asset asset) {
|
|
ImageProvider imageProvider(Asset asset) {
|
|
if (asset.isLocal) {
|
|
if (asset.isLocal) {
|
|
return localImageProvider(asset);
|
|
return localImageProvider(asset);
|
|
@@ -405,7 +491,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
PhotoViewGallery.builder(
|
|
PhotoViewGallery.builder(
|
|
scaleStateChangedCallback: (state) {
|
|
scaleStateChangedCallback: (state) {
|
|
isZoomed.value = state != PhotoViewScaleState.initial;
|
|
isZoomed.value = state != PhotoViewScaleState.initial;
|
|
- showAppBar.value = !isZoomed.value;
|
|
|
|
},
|
|
},
|
|
pageController: controller,
|
|
pageController: controller,
|
|
scrollPhysics: isZoomed.value
|
|
scrollPhysics: isZoomed.value
|
|
@@ -426,6 +511,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
precacheNextImage(value - 1);
|
|
precacheNextImage(value - 1);
|
|
}
|
|
}
|
|
currentIndex.value = value;
|
|
currentIndex.value = value;
|
|
|
|
+ progressValue.value = 0.0;
|
|
|
|
+
|
|
HapticFeedback.selectionClick();
|
|
HapticFeedback.selectionClick();
|
|
},
|
|
},
|
|
loadingBuilder: isLoadPreview.value
|
|
loadingBuilder: isLoadPreview.value
|
|
@@ -493,8 +580,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
localPosition = details.localPosition,
|
|
localPosition = details.localPosition,
|
|
onDragUpdate: (_, details, __) =>
|
|
onDragUpdate: (_, details, __) =>
|
|
handleSwipeUpDown(details),
|
|
handleSwipeUpDown(details),
|
|
- onTapDown: (_, __, ___) =>
|
|
|
|
- showAppBar.value = !showAppBar.value,
|
|
|
|
|
|
+ onTapDown: (_, __, ___) {
|
|
|
|
+ ref.read(showControlsProvider.notifier).toggle();
|
|
|
|
+ },
|
|
imageProvider: provider,
|
|
imageProvider: provider,
|
|
heroAttributes: PhotoViewHeroAttributes(
|
|
heroAttributes: PhotoViewHeroAttributes(
|
|
tag: asset.id,
|
|
tag: asset.id,
|
|
@@ -519,7 +607,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
filterQuality: FilterQuality.high,
|
|
filterQuality: FilterQuality.high,
|
|
maxScale: 1.0,
|
|
maxScale: 1.0,
|
|
minScale: 1.0,
|
|
minScale: 1.0,
|
|
- basePosition: Alignment.bottomCenter,
|
|
|
|
|
|
+ basePosition: Alignment.center,
|
|
child: VideoViewerPage(
|
|
child: VideoViewerPage(
|
|
onPlaying: () => isPlayingVideo.value = true,
|
|
onPlaying: () => isPlayingVideo.value = true,
|
|
onPaused: () => isPlayingVideo.value = false,
|
|
onPaused: () => isPlayingVideo.value = false,
|
|
@@ -559,4 +647,37 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
),
|
|
),
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ 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;
|
|
|
|
+ }
|
|
}
|
|
}
|