mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
Merge branch 'main' into feat/toggle-video-auto-play
This commit is contained in:
commit
611180d259
629 changed files with 29549 additions and 8465 deletions
|
|
@ -0,0 +1,30 @@
|
|||
import 'package:flutter/material.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';
|
||||
|
||||
class AdvancedInfoActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
|
||||
const AdvancedInfoActionButton({super.key, required this.source});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(actionProvider.notifier).troubleshoot(source, context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
maxWidth: 115.0,
|
||||
iconData: Icons.help_outline_rounded,
|
||||
label: "troubleshoot".t(context: context),
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
|
|
@ -58,7 +59,7 @@ class LikeActivityActionButton extends ConsumerWidget {
|
|||
label: "like".t(context: context),
|
||||
menuItem: menuItem,
|
||||
),
|
||||
error: (error, stack) => Text("Error: $error"),
|
||||
error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
|||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/album_filter.utils.dart';
|
||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||
|
|
@ -39,8 +40,13 @@ class AlbumSelector extends ConsumerStatefulWidget {
|
|||
class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
bool isGrid = false;
|
||||
final searchController = TextEditingController();
|
||||
QuickFilterMode filterMode = QuickFilterMode.all;
|
||||
final menuController = MenuController();
|
||||
final searchFocusNode = FocusNode();
|
||||
List<RemoteAlbum> sortedAlbums = [];
|
||||
List<RemoteAlbum> shownAlbums = [];
|
||||
|
||||
AlbumFilter filter = AlbumFilter(query: "", mode: QuickFilterMode.all);
|
||||
AlbumSort sort = AlbumSort(mode: RemoteAlbumSortMode.lastModified, isReverse: true);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -52,7 +58,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
|||
});
|
||||
|
||||
searchController.addListener(() {
|
||||
onSearch(searchController.text, filterMode);
|
||||
onSearch(searchController.text, filter.mode);
|
||||
});
|
||||
|
||||
searchFocusNode.addListener(() {
|
||||
|
|
@ -62,9 +68,11 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
|||
});
|
||||
}
|
||||
|
||||
void onSearch(String searchTerm, QuickFilterMode sortMode) {
|
||||
void onSearch(String searchTerm, QuickFilterMode filterMode) {
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
ref.read(remoteAlbumProvider.notifier).searchAlbums(searchTerm, userId, sortMode);
|
||||
filter = filter.copyWith(query: searchTerm, userId: userId, mode: filterMode);
|
||||
|
||||
filterAlbums();
|
||||
}
|
||||
|
||||
Future<void> onRefresh() async {
|
||||
|
|
@ -77,17 +85,60 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
|||
});
|
||||
}
|
||||
|
||||
void changeFilter(QuickFilterMode sortMode) {
|
||||
void changeFilter(QuickFilterMode mode) {
|
||||
setState(() {
|
||||
filterMode = sortMode;
|
||||
filter = filter.copyWith(mode: mode);
|
||||
});
|
||||
|
||||
filterAlbums();
|
||||
}
|
||||
|
||||
Future<void> changeSort(AlbumSort sort) async {
|
||||
setState(() {
|
||||
this.sort = sort;
|
||||
});
|
||||
|
||||
await sortAlbums();
|
||||
}
|
||||
|
||||
void clearSearch() {
|
||||
setState(() {
|
||||
filterMode = QuickFilterMode.all;
|
||||
filter = filter.copyWith(mode: QuickFilterMode.all, query: null);
|
||||
searchController.clear();
|
||||
ref.read(remoteAlbumProvider.notifier).clearSearch();
|
||||
});
|
||||
|
||||
filterAlbums();
|
||||
}
|
||||
|
||||
Future<void> sortAlbums() async {
|
||||
final sorted = await ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.sortAlbums(ref.read(remoteAlbumProvider).albums, sort.mode, isReverse: sort.isReverse);
|
||||
|
||||
setState(() {
|
||||
sortedAlbums = sorted;
|
||||
});
|
||||
|
||||
// we need to re-filter the albums after sorting
|
||||
// so shownAlbums gets updated
|
||||
filterAlbums();
|
||||
}
|
||||
|
||||
Future<void> filterAlbums() async {
|
||||
if (filter.query == null) {
|
||||
setState(() {
|
||||
shownAlbums = sortedAlbums;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final filteredAlbums = ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.searchAlbums(sortedAlbums, filter.query!, filter.userId, filter.mode);
|
||||
|
||||
setState(() {
|
||||
shownAlbums = filteredAlbums;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -100,36 +151,52 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final albums = ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums));
|
||||
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
|
||||
return MultiSliver(
|
||||
children: [
|
||||
_SearchBar(
|
||||
searchController: searchController,
|
||||
searchFocusNode: searchFocusNode,
|
||||
onSearch: onSearch,
|
||||
filterMode: filterMode,
|
||||
onClearSearch: clearSearch,
|
||||
),
|
||||
_QuickFilterButtonRow(
|
||||
filterMode: filterMode,
|
||||
onChangeFilter: changeFilter,
|
||||
onSearch: onSearch,
|
||||
searchController: searchController,
|
||||
),
|
||||
_QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode),
|
||||
isGrid
|
||||
? _AlbumGrid(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
|
||||
: _AlbumList(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
|
||||
],
|
||||
// refilter and sort when albums change
|
||||
ref.listen(remoteAlbumProvider.select((state) => state.albums), (_, _) async {
|
||||
await sortAlbums();
|
||||
});
|
||||
|
||||
return PopScope(
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
menuController.close();
|
||||
},
|
||||
child: MultiSliver(
|
||||
children: [
|
||||
_SearchBar(
|
||||
searchController: searchController,
|
||||
searchFocusNode: searchFocusNode,
|
||||
onSearch: onSearch,
|
||||
filterMode: filter.mode,
|
||||
onClearSearch: clearSearch,
|
||||
),
|
||||
_QuickFilterButtonRow(
|
||||
filterMode: filter.mode,
|
||||
onChangeFilter: changeFilter,
|
||||
onSearch: onSearch,
|
||||
searchController: searchController,
|
||||
),
|
||||
_QuickSortAndViewMode(
|
||||
isGrid: isGrid,
|
||||
onToggleViewMode: toggleViewMode,
|
||||
onSortChanged: changeSort,
|
||||
controller: menuController,
|
||||
),
|
||||
isGrid
|
||||
? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
|
||||
: _AlbumList(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SortButton extends ConsumerStatefulWidget {
|
||||
const _SortButton();
|
||||
const _SortButton(this.onSortChanged, {this.controller});
|
||||
|
||||
final Future<void> Function(AlbumSort) onSortChanged;
|
||||
final MenuController? controller;
|
||||
|
||||
@override
|
||||
ConsumerState<_SortButton> createState() => _SortButtonState();
|
||||
|
|
@ -148,15 +215,15 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
|||
albumSortIsReverse = !albumSortIsReverse;
|
||||
isSorting = true;
|
||||
});
|
||||
await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
|
||||
} else {
|
||||
setState(() {
|
||||
albumSortOption = sortMode;
|
||||
isSorting = true;
|
||||
});
|
||||
await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
|
||||
}
|
||||
|
||||
await widget.onSortChanged.call(AlbumSort(mode: albumSortOption, isReverse: albumSortIsReverse));
|
||||
|
||||
setState(() {
|
||||
isSorting = false;
|
||||
});
|
||||
|
|
@ -165,6 +232,7 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MenuAnchor(
|
||||
controller: widget.controller,
|
||||
style: MenuStyle(
|
||||
elevation: const WidgetStatePropertyAll(1),
|
||||
shape: WidgetStateProperty.all(
|
||||
|
|
@ -394,10 +462,17 @@ class _QuickFilterButton extends StatelessWidget {
|
|||
}
|
||||
|
||||
class _QuickSortAndViewMode extends StatelessWidget {
|
||||
const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode});
|
||||
const _QuickSortAndViewMode({
|
||||
required this.isGrid,
|
||||
required this.onToggleViewMode,
|
||||
required this.onSortChanged,
|
||||
this.controller,
|
||||
});
|
||||
|
||||
final bool isGrid;
|
||||
final VoidCallback onToggleViewMode;
|
||||
final MenuController? controller;
|
||||
final Future<void> Function(AlbumSort) onSortChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -407,7 +482,7 @@ class _QuickSortAndViewMode extends StatelessWidget {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const _SortButton(),
|
||||
_SortButton(onSortChanged, controller: controller),
|
||||
IconButton(
|
||||
icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24),
|
||||
onPressed: onToggleViewMode,
|
||||
|
|
@ -429,9 +504,9 @@ class _AlbumList extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (albums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
return SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')),
|
||||
child: Padding(padding: const EdgeInsets.all(20.0), child: Text('album_search_not_found'.tr())),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -524,9 +599,9 @@ class _AlbumGrid extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (albums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
return SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')),
|
||||
child: Padding(padding: const EdgeInsets.all(20.0), child: Text('album_search_not_found'.tr())),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import 'dart:async';
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart';
|
||||
|
|
@ -29,7 +31,6 @@ import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AssetViewerPage extends StatelessWidget {
|
||||
|
|
@ -52,10 +53,9 @@ class AssetViewerPage extends StatelessWidget {
|
|||
|
||||
class AssetViewer extends ConsumerStatefulWidget {
|
||||
final int initialIndex;
|
||||
final Platform? platform;
|
||||
final int? heroOffset;
|
||||
|
||||
const AssetViewer({super.key, required this.initialIndex, this.platform, this.heroOffset});
|
||||
const AssetViewer({super.key, required this.initialIndex, this.heroOffset});
|
||||
|
||||
@override
|
||||
ConsumerState createState() => _AssetViewerState();
|
||||
|
|
@ -85,7 +85,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
PhotoViewControllerBase? viewController;
|
||||
StreamSubscription? reloadSubscription;
|
||||
|
||||
late Platform platform;
|
||||
late final int heroOffset;
|
||||
late PhotoViewControllerValue initialPhotoViewState;
|
||||
bool? hasDraggedDown;
|
||||
|
|
@ -113,7 +112,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
super.initState();
|
||||
assert(ref.read(currentAssetNotifier) != null, "Current asset should not be null when opening the AssetViewer");
|
||||
pageController = PageController(initialPage: widget.initialIndex);
|
||||
platform = widget.platform ?? const LocalPlatform();
|
||||
totalAssets = ref.read(timelineServiceProvider).totalAssets;
|
||||
bottomSheetController = DraggableScrollableController();
|
||||
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
|
||||
|
|
@ -129,6 +127,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
reloadSubscription?.cancel();
|
||||
_prevPreCacheStream?.removeListener(_dummyListener);
|
||||
_nextPreCacheStream?.removeListener(_dummyListener);
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -596,6 +595,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
// Rebuild the widget when the asset viewer state changes
|
||||
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
|
||||
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
||||
ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
|
||||
ref.watch(assetViewerProvider.select((s) => s.stackIndex));
|
||||
ref.watch(isPlayingMotionVideoProvider);
|
||||
|
|
@ -612,6 +612,15 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
});
|
||||
});
|
||||
|
||||
// Listen for control visibility changes and change system UI mode accordingly
|
||||
ref.listen(assetViewerProvider.select((value) => value.showingControls), (_, showingControls) async {
|
||||
if (showingControls) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||
}
|
||||
});
|
||||
|
||||
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
||||
// Issue: https://github.com/flutter/flutter/issues/109037
|
||||
// TODO: Add a custom scrum builder once the fix lands on stable
|
||||
|
|
@ -626,7 +635,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
gaplessPlayback: true,
|
||||
loadingBuilder: _placeholderBuilder,
|
||||
pageController: pageController,
|
||||
scrollPhysics: platform.isIOS
|
||||
scrollPhysics: CurrentPlatform.isIOS
|
||||
? const FastScrollPhysics() // Use bouncing physics for iOS
|
||||
: const FastClampingScrollPhysics(), // Use heavy physics for Android
|
||||
itemCount: totalAssets,
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||
duration: Durations.short2,
|
||||
child: AnimatedSwitcher(
|
||||
duration: Durations.short4,
|
||||
child: isSheetOpen || isReadonlyModeEnabled
|
||||
child: isSheetOpen
|
||||
? const SizedBox.shrink()
|
||||
: Theme(
|
||||
data: context.themeData.copyWith(
|
||||
|
|
@ -72,14 +72,14 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
child: Container(
|
||||
height: context.padding.bottom + (asset.isVideo ? 160 : 90),
|
||||
color: Colors.black.withAlpha(125),
|
||||
padding: EdgeInsets.only(bottom: context.padding.bottom),
|
||||
padding: EdgeInsets.only(bottom: context.padding.bottom, top: 16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (asset.isVideo) const VideoControls(),
|
||||
if (!isInLockedView) Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
||||
if (!isInLockedView && !isReadonlyModeEnabled)
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart';
|
||||
|
|
@ -14,6 +15,7 @@ import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
|||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
|
@ -41,6 +43,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
|
||||
final buttonContext = ActionButtonContext(
|
||||
asset: asset,
|
||||
|
|
@ -49,6 +52,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||
isTrashEnabled: isTrashEnable,
|
||||
isInLockedView: isInLockedView,
|
||||
currentAlbum: currentAlbum,
|
||||
advancedTroubleshooting: advancedTroubleshooting,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
|
|
@ -122,6 +126,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||
return [fNumber, exposureTime, focalLength, iso].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator);
|
||||
}
|
||||
|
||||
Future<void> _editDateTime(BuildContext context, WidgetRef ref) async {
|
||||
await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final asset = ref.watch(currentAssetNotifier);
|
||||
|
|
@ -132,10 +140,6 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
|
||||
final cameraTitle = _getCameraInfoTitle(exifInfo);
|
||||
|
||||
Future<void> editDateTime() async {
|
||||
await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context);
|
||||
}
|
||||
|
||||
return SliverList.list(
|
||||
children: [
|
||||
// Asset Date and Time
|
||||
|
|
@ -143,7 +147,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||
title: _getDateTime(context, asset),
|
||||
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
trailing: asset.hasRemote ? const Icon(Icons.edit, size: 18) : null,
|
||||
onTap: asset.hasRemote ? () async => await editDateTime() : null,
|
||||
onTap: asset.hasRemote ? () async => await _editDateTime(context, ref) : null,
|
||||
),
|
||||
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo),
|
||||
const SheetPeopleDetails(),
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_act
|
|||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
|
|
@ -38,8 +39,9 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
|
||||
final previousRouteName = ref.watch(previousRouteNameProvider);
|
||||
final tabRoute = ref.watch(tabProvider);
|
||||
final showViewInTimelineButton =
|
||||
previousRouteName != TabShellRoute.name &&
|
||||
(previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) &&
|
||||
previousRouteName != AssetViewerRoute.name &&
|
||||
previousRouteName != null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||
|
|
@ -43,7 +43,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
|||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
const StackActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||
|
|
@ -43,7 +43,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
|||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
const StackActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
|
|
@ -21,6 +23,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_
|
|||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
|
@ -51,6 +54,7 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
|||
Widget build(BuildContext context) {
|
||||
final multiselect = ref.watch(multiSelectProvider);
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
|
||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||
final selectedAssets = multiselect.selectedAssets;
|
||||
|
|
@ -83,24 +87,27 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
|||
|
||||
return BaseBottomSheet(
|
||||
controller: sheetController,
|
||||
initialChildSize: 0.45,
|
||||
initialChildSize: widget.minChildSize ?? 0.15,
|
||||
minChildSize: widget.minChildSize,
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
if (multiselect.selectedAssets.length == 1 && advancedTroubleshooting) ...[
|
||||
const AdvancedInfoActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
const FavoriteActionButton(source: ActionSource.timeline),
|
||||
const DownloadActionButton(source: ActionSource.timeline),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
const StackActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
? const TrashActionButton(source: ActionSource.timeline)
|
||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||
const FavoriteActionButton(source: ActionSource.timeline),
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
const DeleteActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasLocal || multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import 'package:flutter/material.dart';
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||
|
|
@ -16,22 +18,75 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
|
|||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class RemoteAlbumBottomSheet extends ConsumerWidget {
|
||||
class RemoteAlbumBottomSheet extends ConsumerStatefulWidget {
|
||||
final RemoteAlbum album;
|
||||
const RemoteAlbumBottomSheet({super.key, required this.album});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<RemoteAlbumBottomSheet> createState() => _RemoteAlbumBottomSheetState();
|
||||
}
|
||||
|
||||
class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet> {
|
||||
late DraggableScrollableController sheetController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
sheetController = DraggableScrollableController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
sheetController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final multiselect = ref.watch(multiSelectProvider);
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
|
||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||
final selectedAssets = multiselect.selectedAssets;
|
||||
if (selectedAssets.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final addedCount = await ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList());
|
||||
|
||||
if (addedCount != selectedAssets.length) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}),
|
||||
);
|
||||
} else {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}),
|
||||
);
|
||||
}
|
||||
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
}
|
||||
|
||||
Future<void> onKeyboardExpand() {
|
||||
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||
}
|
||||
|
||||
return BaseBottomSheet(
|
||||
initialChildSize: 0.25,
|
||||
maxChildSize: 0.4,
|
||||
controller: sheetController,
|
||||
initialChildSize: 0.22,
|
||||
minChildSize: 0.22,
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
|
|
@ -46,13 +101,17 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
|
|||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
const StackActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: album.id),
|
||||
RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||
],
|
||||
slivers: [
|
||||
const AddToAlbumHeader(),
|
||||
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
|||
|
||||
final operation = cachedOperation;
|
||||
if (operation != null) {
|
||||
this.cachedOperation = null;
|
||||
cachedOperation = null;
|
||||
operation.cancel();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,10 +47,12 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||
MapLibreMapController? mapController;
|
||||
final _reloadMutex = AsyncMutex();
|
||||
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
|
||||
final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debouncer.dispose();
|
||||
bottomSheetOffset.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -157,8 +159,8 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||
return Stack(
|
||||
children: [
|
||||
_Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady),
|
||||
_MyLocationButton(onZoomToLocation: onZoomToLocation),
|
||||
const MapBottomSheet(),
|
||||
_DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset),
|
||||
_DynamicMyLocationButton(onZoomToLocation: onZoomToLocation, bottomSheetOffset: bottomSheetOffset),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -191,21 +193,53 @@ class _Map extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _MyLocationButton extends StatelessWidget {
|
||||
const _MyLocationButton({required this.onZoomToLocation});
|
||||
class _DynamicBottomSheet extends StatefulWidget {
|
||||
final ValueNotifier<double> bottomSheetOffset;
|
||||
|
||||
final VoidCallback onZoomToLocation;
|
||||
const _DynamicBottomSheet({required this.bottomSheetOffset});
|
||||
|
||||
@override
|
||||
State<_DynamicBottomSheet> createState() => _DynamicBottomSheetState();
|
||||
}
|
||||
|
||||
class _DynamicBottomSheetState extends State<_DynamicBottomSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
right: 0,
|
||||
bottom: context.padding.bottom + 16,
|
||||
child: ElevatedButton(
|
||||
onPressed: onZoomToLocation,
|
||||
style: ElevatedButton.styleFrom(shape: const CircleBorder()),
|
||||
child: const Icon(Icons.my_location),
|
||||
),
|
||||
return NotificationListener<DraggableScrollableNotification>(
|
||||
onNotification: (notification) {
|
||||
widget.bottomSheetOffset.value = notification.extent;
|
||||
return true;
|
||||
},
|
||||
child: const MapBottomSheet(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DynamicMyLocationButton extends StatelessWidget {
|
||||
const _DynamicMyLocationButton({required this.onZoomToLocation, required this.bottomSheetOffset});
|
||||
|
||||
final VoidCallback onZoomToLocation;
|
||||
final ValueNotifier<double> bottomSheetOffset;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<double>(
|
||||
valueListenable: bottomSheetOffset,
|
||||
builder: (context, offset, child) {
|
||||
return Positioned(
|
||||
right: 16,
|
||||
bottom: context.height * (offset - 0.02) + context.padding.bottom,
|
||||
child: AnimatedOpacity(
|
||||
opacity: offset < 0.8 ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: ElevatedButton(
|
||||
onPressed: onZoomToLocation,
|
||||
style: ElevatedButton.styleFrom(shape: const CircleBorder()),
|
||||
child: const Icon(Icons.my_location),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
|
|||
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:scroll_date_picker/scroll_date_picker.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget {
|
||||
final DriftPerson person;
|
||||
|
|
@ -36,7 +37,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonBirthdayEdi
|
|||
context.pop<DateTime>(_selectedDate);
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint('Error updating birthday: $error');
|
||||
dPrint(() => 'Error updating birthday: $error');
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
class DriftPersonNameEditForm extends ConsumerStatefulWidget {
|
||||
final DriftPerson person;
|
||||
|
|
@ -34,7 +35,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonNameEditFor
|
|||
context.pop<String>(newName);
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint('Error updating name: $error');
|
||||
dPrint(() => 'Error updating name: $error');
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import 'dart:async';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:intl/intl.dart' hide TextDirection;
|
||||
|
||||
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
||||
|
|
@ -44,7 +46,7 @@ class Scrubber extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
List<_Segment> _buildSegments({required List<Segment> layoutSegments, required double timelineHeight}) {
|
||||
const double offsetThreshold = 20.0;
|
||||
const double offsetThreshold = 40.0;
|
||||
|
||||
final segments = <_Segment>[];
|
||||
if (layoutSegments.isEmpty || layoutSegments.first.bucket is! TimeBucket) {
|
||||
|
|
@ -74,9 +76,11 @@ List<_Segment> _buildSegments({required List<Segment> layoutSegments, required d
|
|||
}
|
||||
|
||||
class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixin {
|
||||
String? _lastLabel;
|
||||
double _thumbTopOffset = 0.0;
|
||||
bool _isDragging = false;
|
||||
List<_Segment> _segments = [];
|
||||
int _monthCount = 0;
|
||||
|
||||
late AnimationController _thumbAnimationController;
|
||||
Timer? _fadeOutTimer;
|
||||
|
|
@ -103,6 +107,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
|||
_thumbAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
|
||||
_thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastEaseInToSlowEaseOut);
|
||||
_labelAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
|
||||
_monthCount = getMonthCount();
|
||||
|
||||
_labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn);
|
||||
}
|
||||
|
|
@ -119,6 +124,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
|||
|
||||
if (oldWidget.layoutSegments.lastOrNull?.endOffset != widget.layoutSegments.lastOrNull?.endOffset) {
|
||||
_segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight);
|
||||
_monthCount = getMonthCount();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +144,10 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
|||
});
|
||||
}
|
||||
|
||||
int getMonthCount() {
|
||||
return _segments.map((e) => "${e.date.month}_${e.date.year}").toSet().length;
|
||||
}
|
||||
|
||||
bool _onScrollNotification(ScrollNotification notification) {
|
||||
if (_isDragging) {
|
||||
// If the user is dragging the thumb, we don't want to update the position
|
||||
|
|
@ -167,11 +177,15 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
|||
}
|
||||
|
||||
void _onDragStart(DragStartDetails _) {
|
||||
ref.read(timelineStateProvider.notifier).setScrubbing(true);
|
||||
if (_monthCount >= kMinMonthsToEnableScrubberSnap) {
|
||||
ref.read(timelineStateProvider.notifier).setScrubbing(true);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isDragging = true;
|
||||
_labelAnimationController.forward();
|
||||
_fadeOutTimer?.cancel();
|
||||
_lastLabel = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -188,6 +202,20 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
|||
final nearestMonthSegment = _findNearestMonthSegment(dragPosition);
|
||||
|
||||
if (nearestMonthSegment != null) {
|
||||
final label = nearestMonthSegment.scrollLabel;
|
||||
if (_lastLabel != label) {
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
_lastLabel = label;
|
||||
}
|
||||
}
|
||||
|
||||
if (_monthCount < kMinMonthsToEnableScrubberSnap) {
|
||||
// If there are less than kMinMonthsToEnableScrubberSnap months, we don't need to snap to segments
|
||||
setState(() {
|
||||
_thumbTopOffset = dragPosition;
|
||||
_scrollController.jumpTo((dragPosition / _scrubberHeight) * _scrollController.position.maxScrollExtent);
|
||||
});
|
||||
} else if (nearestMonthSegment != null) {
|
||||
_snapToSegment(nearestMonthSegment);
|
||||
}
|
||||
}
|
||||
|
|
@ -362,7 +390,7 @@ class _SegmentWidget extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 12.0),
|
||||
margin: const EdgeInsets.only(right: 36.0),
|
||||
child: Material(
|
||||
color: context.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||
|
|
|
|||
|
|
@ -32,17 +32,17 @@ class Timeline extends StatelessWidget {
|
|||
super.key,
|
||||
this.topSliverWidget,
|
||||
this.topSliverWidgetHeight,
|
||||
this.showStorageIndicator,
|
||||
this.showStorageIndicator = false,
|
||||
this.withStack = false,
|
||||
this.appBar = const ImmichSliverAppBar(floating: true, pinned: false, snap: false),
|
||||
this.bottomSheet = const GeneralBottomSheet(),
|
||||
this.bottomSheet = const GeneralBottomSheet(minChildSize: 0.18),
|
||||
this.groupBy,
|
||||
this.withScrubber = true,
|
||||
});
|
||||
|
||||
final Widget? topSliverWidget;
|
||||
final double? topSliverWidgetHeight;
|
||||
final bool? showStorageIndicator;
|
||||
final bool showStorageIndicator;
|
||||
final Widget? appBar;
|
||||
final Widget? bottomSheet;
|
||||
final bool withStack;
|
||||
|
|
@ -356,7 +356,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||
children: [
|
||||
timeline,
|
||||
if (!isSelectionMode && isMultiSelectEnabled) ...[
|
||||
const Positioned(top: 60, left: 25, child: _MultiSelectStatusButton()),
|
||||
Positioned(
|
||||
top: MediaQuery.paddingOf(context).top,
|
||||
left: 25,
|
||||
child: const SizedBox(
|
||||
height: kToolbarHeight,
|
||||
child: Center(child: _MultiSelectStatusButton()),
|
||||
),
|
||||
),
|
||||
if (widget.bottomSheet != null) widget.bottomSheet!,
|
||||
],
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue