1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-26 10:50:29 +02:00

feat(mobile): custom video player controls (#2960)

* Remove toggle fullscreen button

* Implement custom video player controls

* Move Padding into Container
This commit is contained in:
Sergey Kondrikov 2023-06-26 18:27:47 +03:00 committed by GitHub
parent 99f85fb359
commit fb2cfcb640
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 618 additions and 69 deletions

View File

@ -0,0 +1,21 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
final showControlsProvider = StateNotifierProvider<ShowControls, bool>((ref) {
return ShowControls(ref);
});
class ShowControls extends StateNotifier<bool> {
ShowControls(this.ref) : super(true);
final Ref ref;
bool get show => state;
set show(bool value) {
state = value;
}
void toggle() {
state = !state;
}
}

View File

@ -0,0 +1,46 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
class VideoPlaybackControls {
VideoPlaybackControls({required this.position, required this.mute});
final double position;
final bool mute;
}
final videoPlayerControlsProvider =
StateNotifierProvider<VideoPlayerControls, VideoPlaybackControls>((ref) {
return VideoPlayerControls(ref);
});
class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
VideoPlayerControls(this.ref)
: super(
VideoPlaybackControls(
position: 0,
mute: false,
),
);
final Ref ref;
VideoPlaybackControls get value => state;
set value(VideoPlaybackControls value) {
state = value;
}
double get position => state.position;
bool get mute => state.mute;
set position(double value) {
state = VideoPlaybackControls(position: value, mute: state.mute);
}
set mute(bool value) {
state = VideoPlaybackControls(position: state.position, mute: value);
}
void toggleMute() {
state = VideoPlaybackControls(position: state.position, mute: !state.mute);
}
}

View File

@ -0,0 +1,35 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
class VideoPlaybackValue {
VideoPlaybackValue({required this.position, required this.duration});
final Duration position;
final Duration duration;
}
final videoPlaybackValueProvider =
StateNotifierProvider<VideoPlaybackValueState, VideoPlaybackValue>((ref) {
return VideoPlaybackValueState(ref);
});
class VideoPlaybackValueState extends StateNotifier<VideoPlaybackValue> {
VideoPlaybackValueState(this.ref)
: super(
VideoPlaybackValue(
position: Duration.zero,
duration: Duration.zero,
),
);
final Ref ref;
VideoPlaybackValue get value => state;
set value(VideoPlaybackValue value) {
state = value;
}
set position(Duration value) {
state = VideoPlaybackValue(position: value, duration: state.duration);
}
}

View File

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
/// A widget that animates implicitly between a play and a pause icon.
class AnimatedPlayPause extends StatefulWidget {
const AnimatedPlayPause({
Key? key,
required this.playing,
this.size,
this.color,
}) : super(key: key);
final double? size;
final bool playing;
final Color? color;
@override
State<StatefulWidget> createState() => AnimatedPlayPauseState();
}
class AnimatedPlayPauseState extends State<AnimatedPlayPause>
with SingleTickerProviderStateMixin {
late final animationController = AnimationController(
vsync: this,
value: widget.playing ? 1 : 0,
duration: const Duration(milliseconds: 300),
);
@override
void didUpdateWidget(AnimatedPlayPause oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.playing != oldWidget.playing) {
if (widget.playing) {
animationController.forward();
} else {
animationController.reverse();
}
}
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedIcon(
color: widget.color,
size: widget.size,
icon: AnimatedIcons.play_pause,
progress: animationController,
),
);
}
}

View File

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/animated_play_pause.dart';
class CenterPlayButton extends StatelessWidget {
const CenterPlayButton({
Key? key,
required this.backgroundColor,
this.iconColor,
required this.show,
required this.isPlaying,
required this.isFinished,
this.onPressed,
}) : super(key: key);
final Color backgroundColor;
final Color? iconColor;
final bool show;
final bool isPlaying;
final bool isFinished;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return ColoredBox(
color: Colors.transparent,
child: Center(
child: UnconstrainedBox(
child: AnimatedOpacity(
opacity: show ? 1.0 : 0.0,
duration: const Duration(milliseconds: 100),
child: DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
shape: BoxShape.circle,
),
child: IconButton(
iconSize: 32,
padding: const EdgeInsets.all(12.0),
icon: isFinished
? Icon(Icons.replay, color: iconColor)
: AnimatedPlayPause(
color: iconColor,
playing: isPlaying,
),
onPressed: onPressed,
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,207 @@
import 'dart:async';
import 'package:chewie/chewie.dart';
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';
import 'package:immich_mobile/modules/asset_viewer/ui/center_play_button.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:video_player/video_player.dart';
class VideoPlayerControls extends ConsumerStatefulWidget {
const VideoPlayerControls({
Key? key,
}) : super(key: key);
@override
VideoPlayerControlsState createState() => VideoPlayerControlsState();
}
class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
with SingleTickerProviderStateMixin {
late VideoPlayerController controller;
late VideoPlayerValue _latestValue;
bool _displayBufferingIndicator = false;
double? _latestVolume;
Timer? _hideTimer;
ChewieController? _chewieController;
ChewieController get chewieController => _chewieController!;
@override
Widget build(BuildContext context) {
ref.listen(videoPlayerControlsProvider.select((value) => value.mute),
(_, value) {
_mute(value);
_cancelAndRestartTimer();
});
ref.listen(videoPlayerControlsProvider.select((value) => value.position),
(_, position) {
_seekTo(position);
_cancelAndRestartTimer();
});
if (_latestValue.hasError) {
return chewieController.errorBuilder?.call(
context,
chewieController.videoPlayerController.value.errorDescription!,
) ??
const Center(
child: Icon(
Icons.error,
color: Colors.white,
size: 42,
),
);
}
return GestureDetector(
onTap: () => _cancelAndRestartTimer(),
child: AbsorbPointer(
absorbing: !ref.watch(showControlsProvider),
child: Stack(
children: [
if (_displayBufferingIndicator)
const Center(
child: ImmichLoadingIndicator(),
)
else
_buildHitArea(),
],
),
),
);
}
@override
void dispose() {
_dispose();
super.dispose();
}
void _dispose() {
controller.removeListener(_updateState);
_hideTimer?.cancel();
}
@override
void didChangeDependencies() {
final oldController = _chewieController;
_chewieController = ChewieController.of(context);
controller = chewieController.videoPlayerController;
if (oldController != chewieController) {
_dispose();
_initialize();
}
super.didChangeDependencies();
}
Widget _buildHitArea() {
final bool isFinished = _latestValue.position >= _latestValue.duration;
return GestureDetector(
onTap: () {
if (_latestValue.isPlaying) {
ref.read(showControlsProvider.notifier).show = false;
} else {
_playPause();
ref.read(showControlsProvider.notifier).show = false;
}
},
child: CenterPlayButton(
backgroundColor: Colors.black54,
iconColor: Colors.white,
isFinished: isFinished,
isPlaying: controller.value.isPlaying,
show: ref.watch(showControlsProvider),
onPressed: _playPause,
),
);
}
void _cancelAndRestartTimer() {
_hideTimer?.cancel();
_startHideTimer();
ref.read(showControlsProvider.notifier).show = true;
}
Future<void> _initialize() async {
_mute(ref.read(videoPlayerControlsProvider.select((value) => value.mute)));
controller.addListener(_updateState);
_latestValue = controller.value;
if (controller.value.isPlaying || chewieController.autoPlay) {
_startHideTimer();
}
}
void _playPause() {
final isFinished = _latestValue.position >= _latestValue.duration;
setState(() {
if (controller.value.isPlaying) {
ref.read(showControlsProvider.notifier).show = true;
_hideTimer?.cancel();
controller.pause();
} else {
_cancelAndRestartTimer();
if (!controller.value.isInitialized) {
controller.initialize().then((_) {
controller.play();
});
} else {
if (isFinished) {
controller.seekTo(Duration.zero);
}
controller.play();
}
}
});
}
void _startHideTimer() {
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
? ChewieController.defaultHideControlsTimer
: chewieController.hideControlsTimer;
_hideTimer = Timer(hideControlsTimer, () {
ref.read(showControlsProvider.notifier).show = false;
});
}
void _updateState() {
if (!mounted) return;
_displayBufferingIndicator = controller.value.isBuffering;
setState(() {
_latestValue = controller.value;
ref.read(videoPlaybackValueProvider.notifier).value = VideoPlaybackValue(
position: _latestValue.position,
duration: _latestValue.duration,
);
});
}
void _mute(bool mute) {
if (mute) {
_latestVolume = controller.value.volume;
controller.setVolume(0);
} else {
controller.setVolume(_latestVolume ?? 0.5);
}
}
void _seekTo(double position) {
final Duration pos = controller.value.duration * (position / 100.0);
if (pos != controller.value.position) {
controller.seekTo(pos);
}
}
}

View File

@ -6,8 +6,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 +52,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 +63,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 +271,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,17 +303,99 @@ class GalleryViewerPage extends HookConsumerWidget {
); );
} }
buildBottomBar() { Widget buildProgressBar() {
final show = (showAppBar.value || // onTap has the final say final playerValue = ref.watch(videoPlaybackValueProvider);
(showAppBar.value && !isZoomed.value)) &&
!isPlayingVideo.value;
return Expanded(
child: Slider(
value: playerValue.duration == Duration.zero
? 0.0
: playerValue.position.inMicroseconds /
playerValue.duration.inMicroseconds *
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, opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
child: BottomNavigationBar( 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(),
],
),
),
),
),
BottomNavigationBar(
backgroundColor: Colors.black.withOpacity(0.4), backgroundColor: Colors.black.withOpacity(0.4),
unselectedIconTheme: const IconThemeData(color: Colors.white), unselectedIconTheme: const IconThemeData(color: Colors.white),
selectedIconTheme: const IconThemeData(color: Colors.white), selectedIconTheme: const IconThemeData(color: Colors.white),
@ -368,10 +440,20 @@ class GalleryViewerPage extends HookConsumerWidget {
} }
}, },
), ),
],
),
), ),
); );
} }
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 +487,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 +507,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 +576,9 @@ class GalleryViewerPage extends HookConsumerWidget {
localPosition = details.localPosition, localPosition = details.localPosition,
onDragUpdate: (_, details, __) => onDragUpdate: (_, details, __) =>
handleSwipeUpDown(details), handleSwipeUpDown(details),
onTapDown: (_, __, ___) => onTapDown: (_, __, ___) {
showAppBar.value = !showAppBar.value, ref.read(showControlsProvider.notifier).toggle();
},
imageProvider: provider, imageProvider: provider,
heroAttributes: PhotoViewHeroAttributes( heroAttributes: PhotoViewHeroAttributes(
tag: asset.id, tag: asset.id,
@ -519,7 +603,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 +643,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;
}
} }

View File

@ -5,11 +5,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:chewie/chewie.dart'; import 'package:chewie/chewie.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.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/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/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:wakelock/wakelock.dart';
// ignore: must_be_immutable // ignore: must_be_immutable
class VideoViewerPage extends HookConsumerWidget { class VideoViewerPage extends HookConsumerWidget {
@ -130,13 +132,16 @@ class _VideoPlayerState extends State<VideoPlayer> {
videoPlayerController.addListener(() { videoPlayerController.addListener(() {
if (videoPlayerController.value.isInitialized) { if (videoPlayerController.value.isInitialized) {
if (videoPlayerController.value.isPlaying) { if (videoPlayerController.value.isPlaying) {
Wakelock.enable();
widget.onPlaying?.call(); widget.onPlaying?.call();
} else if (!videoPlayerController.value.isPlaying) { } else if (!videoPlayerController.value.isPlaying) {
Wakelock.disable();
widget.onPaused?.call(); widget.onPaused?.call();
} }
if (videoPlayerController.value.position == if (videoPlayerController.value.position ==
videoPlayerController.value.duration) { videoPlayerController.value.duration) {
Wakelock.disable();
widget.onVideoEnded(); widget.onVideoEnded();
} }
} }
@ -170,9 +175,10 @@ class _VideoPlayerState extends State<VideoPlayer> {
videoPlayerController: videoPlayerController, videoPlayerController: videoPlayerController,
autoPlay: true, autoPlay: true,
autoInitialize: true, autoInitialize: true,
allowFullScreen: true, allowFullScreen: false,
allowedScreenSleep: false, allowedScreenSleep: false,
showControls: !widget.isMotionVideo, showControls: !widget.isMotionVideo,
customControls: const VideoPlayerControls(),
hideControlsTimer: const Duration(seconds: 5), hideControlsTimer: const Duration(seconds: 5),
); );
} }

View File

@ -24,6 +24,10 @@ ThemeData base = ThemeData(
chipTheme: const ChipThemeData( chipTheme: const ChipThemeData(
side: BorderSide.none, side: BorderSide.none,
), ),
sliderTheme: const SliderThemeData(
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7),
trackHeight: 2.0,
),
); );
ThemeData immichLightTheme = ThemeData( ThemeData immichLightTheme = ThemeData(
@ -99,6 +103,7 @@ ThemeData immichLightTheme = ThemeData(
), ),
), ),
chipTheme: base.chipTheme, chipTheme: base.chipTheme,
sliderTheme: base.sliderTheme,
popupMenuTheme: const PopupMenuThemeData( popupMenuTheme: const PopupMenuThemeData(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)), borderRadius: BorderRadius.all(Radius.circular(10)),
@ -218,6 +223,7 @@ ThemeData immichDarkTheme = ThemeData(
), ),
), ),
chipTheme: base.chipTheme, chipTheme: base.chipTheme,
sliderTheme: base.sliderTheme,
popupMenuTheme: const PopupMenuThemeData( popupMenuTheme: const PopupMenuThemeData(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)), borderRadius: BorderRadius.all(Radius.circular(10)),

View File

@ -1409,7 +1409,7 @@ packages:
source: hosted source: hosted
version: "11.3.0" version: "11.3.0"
wakelock: wakelock:
dependency: transitive dependency: "direct main"
description: description:
name: wakelock name: wakelock
sha256: "769ecf42eb2d07128407b50cb93d7c10bd2ee48f0276ef0119db1d25cc2f87db" sha256: "769ecf42eb2d07128407b50cb93d7c10bd2ee48f0276ef0119db1d25cc2f87db"

View File

@ -47,6 +47,7 @@ dependencies:
permission_handler: ^10.2.0 permission_handler: ^10.2.0
device_info_plus: ^8.1.0 device_info_plus: ^8.1.0
crypto: ^3.0.3 # TODO remove once native crypto is used on iOS crypto: ^3.0.3 # TODO remove once native crypto is used on iOS
wakelock: ^0.6.2
openapi: openapi:
path: openapi path: openapi