mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
refactor: reduce timeline rebuilds (#19704)
* reduce timeline rebuilds * feat: adds bottom sheet map and actions (#19692) * adds bottom sheet map and actions * PR feedbacks * only reload the asset viewer if asset is changed * styling tweak --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com> * rename singleton and remove event prefix --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
b00d44a00c
commit
181efb9010
@ -1,5 +1,5 @@
|
|||||||
part 'remote_asset.model.dart';
|
|
||||||
part 'local_asset.model.dart';
|
part 'local_asset.model.dart';
|
||||||
|
part 'remote_asset.model.dart';
|
||||||
|
|
||||||
enum AssetType {
|
enum AssetType {
|
||||||
// do not change this order!
|
// do not change this order!
|
||||||
@ -48,6 +48,13 @@ sealed class BaseAsset {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get hasRemote =>
|
||||||
|
storage == AssetState.remote || storage == AssetState.merged;
|
||||||
|
bool get hasLocal =>
|
||||||
|
storage == AssetState.local || storage == AssetState.merged;
|
||||||
|
bool get isLocalOnly => storage == AssetState.local;
|
||||||
|
bool get isRemoteOnly => storage == AssetState.remote;
|
||||||
|
|
||||||
// Overridden in subclasses
|
// Overridden in subclasses
|
||||||
AssetState get storage;
|
AssetState get storage;
|
||||||
String get heroTag;
|
String get heroTag;
|
||||||
|
@ -25,8 +25,6 @@ class LocalAsset extends BaseAsset {
|
|||||||
@override
|
@override
|
||||||
String get heroTag => '${id}_${remoteId ?? checksum}';
|
String get heroTag => '${id}_${remoteId ?? checksum}';
|
||||||
|
|
||||||
bool get hasRemote => remoteId != null;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''LocalAsset {
|
return '''LocalAsset {
|
||||||
|
@ -39,8 +39,6 @@ class RemoteAsset extends BaseAsset {
|
|||||||
@override
|
@override
|
||||||
String get heroTag => '${localId ?? checksum}_$id';
|
String get heroTag => '${localId ?? checksum}_$id';
|
||||||
|
|
||||||
bool get hasLocal => localId != null;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''Asset {
|
return '''Asset {
|
||||||
|
@ -6,6 +6,7 @@ enum Setting<T> {
|
|||||||
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
|
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
|
||||||
loadOriginal<bool>(StoreKey.loadOriginal, false),
|
loadOriginal<bool>(StoreKey.loadOriginal, false),
|
||||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
|
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
|
||||||
|
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
|
||||||
;
|
;
|
||||||
|
|
||||||
const Setting(this.storeKey, this.defaultValue);
|
const Setting(this.storeKey, this.defaultValue);
|
||||||
|
@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||||
|
|
||||||
@ -68,7 +69,7 @@ class TimelineService {
|
|||||||
_bucketSubscription = _bucketSource().listen((buckets) {
|
_bucketSubscription = _bucketSource().listen((buckets) {
|
||||||
_totalAssets =
|
_totalAssets =
|
||||||
buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
|
buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
|
||||||
unawaited(reloadBucket());
|
unawaited(_reloadBucket());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,8 +80,9 @@ class TimelineService {
|
|||||||
|
|
||||||
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
|
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
|
||||||
|
|
||||||
Future<void> reloadBucket() => _mutex.run(() async {
|
Future<void> _reloadBucket() => _mutex.run(() async {
|
||||||
_buffer = await _assetSource(_bufferOffset, _buffer.length);
|
_buffer = await _assetSource(_bufferOffset, _buffer.length);
|
||||||
|
EventStream.shared.emit(const TimelineReloadEvent());
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<List<BaseAsset>> loadAssets(int index, int count) =>
|
Future<List<BaseAsset>> loadAssets(int index, int count) =>
|
||||||
|
48
mobile/lib/domain/utils/event_stream.dart
Normal file
48
mobile/lib/domain/utils/event_stream.dart
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
sealed class Event {
|
||||||
|
const Event();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimelineReloadEvent extends Event {
|
||||||
|
const TimelineReloadEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventStream {
|
||||||
|
EventStream._();
|
||||||
|
|
||||||
|
static final EventStream shared = EventStream._();
|
||||||
|
|
||||||
|
final StreamController<Event> _controller =
|
||||||
|
StreamController<Event>.broadcast();
|
||||||
|
|
||||||
|
void emit(Event event) {
|
||||||
|
_controller.add(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<T> where<T extends Event>() {
|
||||||
|
if (T == Event) {
|
||||||
|
return _controller.stream as Stream<T>;
|
||||||
|
}
|
||||||
|
return _controller.stream.where((event) => event is T).cast<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription<T> listen<T extends Event>(
|
||||||
|
void Function(T event)? onData, {
|
||||||
|
Function? onError,
|
||||||
|
void Function()? onDone,
|
||||||
|
bool? cancelOnError,
|
||||||
|
}) {
|
||||||
|
return where<T>().listen(
|
||||||
|
onData,
|
||||||
|
onError: onError,
|
||||||
|
onDone: onDone,
|
||||||
|
cancelOnError: cancelOnError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closes the stream controller
|
||||||
|
void dispose() {
|
||||||
|
_controller.close();
|
||||||
|
}
|
||||||
|
}
|
@ -42,22 +42,6 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onNavigationSelected(TabsRouter router, int index) {
|
|
||||||
// On Photos page menu tapped
|
|
||||||
if (router.activeIndex == 0 && index == 0) {
|
|
||||||
scrollToTopNotifierProvider.scrollToTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// On Search page tapped
|
|
||||||
if (router.activeIndex == 1 && index == 1) {
|
|
||||||
ref.read(searchInputFocusProvider).requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
|
||||||
router.setActiveIndex(index);
|
|
||||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
final navigationDestinations = [
|
final navigationDestinations = [
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'photos'.tr(),
|
label: 'photos'.tr(),
|
||||||
@ -110,15 +94,6 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget bottomNavigationBar(TabsRouter tabsRouter) {
|
|
||||||
return NavigationBar(
|
|
||||||
selectedIndex: tabsRouter.activeIndex,
|
|
||||||
onDestinationSelected: (index) =>
|
|
||||||
onNavigationSelected(tabsRouter, index),
|
|
||||||
destinations: navigationDestinations,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget navigationRail(TabsRouter tabsRouter) {
|
Widget navigationRail(TabsRouter tabsRouter) {
|
||||||
return NavigationRail(
|
return NavigationRail(
|
||||||
destinations: navigationDestinations
|
destinations: navigationDestinations
|
||||||
@ -131,15 +106,13 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
onDestinationSelected: (index) =>
|
onDestinationSelected: (index) =>
|
||||||
onNavigationSelected(tabsRouter, index),
|
_onNavigationSelected(tabsRouter, index, ref),
|
||||||
selectedIndex: tabsRouter.activeIndex,
|
selectedIndex: tabsRouter.activeIndex,
|
||||||
labelType: NavigationRailLabelType.all,
|
labelType: NavigationRailLabelType.all,
|
||||||
groupAlignment: 0.0,
|
groupAlignment: 0.0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final multiselectEnabled =
|
|
||||||
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
|
||||||
return AutoTabsRouter(
|
return AutoTabsRouter(
|
||||||
routes: [
|
routes: [
|
||||||
const MainTimelineRoute(),
|
const MainTimelineRoute(),
|
||||||
@ -173,12 +146,57 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
: heroedChild,
|
: heroedChild,
|
||||||
bottomNavigationBar: multiselectEnabled || isScreenLandscape
|
bottomNavigationBar: _BottomNavigationBar(
|
||||||
? null
|
tabsRouter: tabsRouter,
|
||||||
: bottomNavigationBar(tabsRouter),
|
destinations: navigationDestinations,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
||||||
|
// On Photos page menu tapped
|
||||||
|
if (router.activeIndex == 0 && index == 0) {
|
||||||
|
scrollToTopNotifierProvider.scrollToTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Search page tapped
|
||||||
|
if (router.activeIndex == 1 && index == 1) {
|
||||||
|
ref.read(searchInputFocusProvider).requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||||
|
router.setActiveIndex(index);
|
||||||
|
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomNavigationBar extends ConsumerWidget {
|
||||||
|
const _BottomNavigationBar({
|
||||||
|
required this.tabsRouter,
|
||||||
|
required this.destinations,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Widget> destinations;
|
||||||
|
final TabsRouter tabsRouter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||||
|
final isMultiselectEnabled =
|
||||||
|
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||||
|
|
||||||
|
if (isScreenLandscape || isMultiselectEnabled) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return NavigationBar(
|
||||||
|
selectedIndex: tabsRouter.activeIndex,
|
||||||
|
onDestinationSelected: (index) =>
|
||||||
|
_onNavigationSelected(tabsRouter, index, ref),
|
||||||
|
destinations: destinations,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.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/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
@ -20,7 +19,6 @@ class ArchiveActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).archive(source);
|
final result = await ref.read(actionProvider.notifier).archive(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'archive_action_prompt'.t(
|
final successMessage = 'archive_action_prompt'.t(
|
||||||
|
@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.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/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
@ -20,7 +19,6 @@ class DeletePermanentActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).delete(source);
|
final result = await ref.read(actionProvider.notifier).delete(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'delete_action_prompt'.t(
|
final successMessage = 'delete_action_prompt'.t(
|
||||||
|
@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.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/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
@ -20,7 +19,6 @@ class FavoriteActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).favorite(source);
|
final result = await ref.read(actionProvider.notifier).favorite(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'favorite_action_prompt'.t(
|
final successMessage = 'favorite_action_prompt'.t(
|
||||||
|
@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.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/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
@ -21,7 +20,6 @@ class MoveToLockFolderActionButton extends ConsumerWidget {
|
|||||||
|
|
||||||
final result =
|
final result =
|
||||||
await ref.read(actionProvider.notifier).moveToLockFolder(source);
|
await ref.read(actionProvider.notifier).moveToLockFolder(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'move_to_lock_folder_action_prompt'.t(
|
final successMessage = 'move_to_lock_folder_action_prompt'.t(
|
||||||
|
@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.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/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
@ -21,7 +20,6 @@ class RemoveFromLockFolderActionButton extends ConsumerWidget {
|
|||||||
|
|
||||||
final result =
|
final result =
|
||||||
await ref.read(actionProvider.notifier).removeFromLockFolder(source);
|
await ref.read(actionProvider.notifier).removeFromLockFolder(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'remove_from_lock_folder_action_prompt'.t(
|
final successMessage = 'remove_from_lock_folder_action_prompt'.t(
|
||||||
|
@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.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/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
@ -20,7 +19,6 @@ class TrashActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).trash(source);
|
final result = await ref.read(actionProvider.notifier).trash(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'trash_action_prompt'.t(
|
final successMessage = 'trash_action_prompt'.t(
|
@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.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/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
@ -20,7 +19,6 @@ class UnarchiveActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).unArchive(source);
|
final result = await ref.read(actionProvider.notifier).unArchive(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'unarchive_action_prompt'.t(
|
final successMessage = 'unarchive_action_prompt'.t(
|
||||||
|
@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.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/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
@ -20,7 +19,6 @@ class UnFavoriteActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).unFavorite(source);
|
final result = await ref.read(actionProvider.notifier).unFavorite(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'unfavorite_action_prompt'.t(
|
final successMessage = 'unfavorite_action_prompt'.t(
|
||||||
|
@ -4,9 +4,10 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/services/timeline.service.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/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
@ -60,6 +61,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
PersistentBottomSheetController? sheetCloseNotifier;
|
PersistentBottomSheetController? sheetCloseNotifier;
|
||||||
// PhotoViewGallery takes care of disposing it's controllers
|
// PhotoViewGallery takes care of disposing it's controllers
|
||||||
PhotoViewControllerBase? viewController;
|
PhotoViewControllerBase? viewController;
|
||||||
|
StreamSubscription? _reloadSubscription;
|
||||||
|
|
||||||
late Platform platform;
|
late Platform platform;
|
||||||
late PhotoViewControllerValue initialPhotoViewState;
|
late PhotoViewControllerValue initialPhotoViewState;
|
||||||
@ -88,6 +90,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_onAssetChanged(widget.initialIndex);
|
_onAssetChanged(widget.initialIndex);
|
||||||
});
|
});
|
||||||
|
_reloadSubscription =
|
||||||
|
EventStream.shared.listen<TimelineReloadEvent>(_onTimelineReload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -95,9 +99,31 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
pageController.dispose();
|
pageController.dispose();
|
||||||
bottomSheetController.dispose();
|
bottomSheetController.dispose();
|
||||||
_cancelTimers();
|
_cancelTimers();
|
||||||
|
_reloadSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onTimelineReload(_) {
|
||||||
|
setState(() {
|
||||||
|
totalAssets = ref.read(timelineServiceProvider).totalAssets;
|
||||||
|
if (totalAssets == 0) {
|
||||||
|
context.maybePop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final index = pageController.page?.round() ?? 0;
|
||||||
|
final newAsset = ref.read(timelineServiceProvider).getAsset(index);
|
||||||
|
final currentAsset = ref.read(currentAssetNotifier);
|
||||||
|
// Do not reload / close the bottom sheet if the asset has not changed
|
||||||
|
if (newAsset.heroTag == currentAsset.heroTag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onAssetChanged(pageController.page!.round());
|
||||||
|
sheetCloseNotifier?.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Color get backgroundColor {
|
Color get backgroundColor {
|
||||||
if (showingBottomSheet && !context.isDarkTheme) {
|
if (showingBottomSheet && !context.isDarkTheme) {
|
||||||
return Colors.white;
|
return Colors.white;
|
||||||
@ -186,11 +212,12 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
void _onDragStart(
|
void _onDragStart(
|
||||||
_,
|
_,
|
||||||
DragStartDetails details,
|
DragStartDetails details,
|
||||||
PhotoViewControllerValue value,
|
PhotoViewControllerBase controller,
|
||||||
PhotoViewScaleStateController scaleStateController,
|
PhotoViewScaleStateController scaleStateController,
|
||||||
) {
|
) {
|
||||||
|
viewController = controller;
|
||||||
dragDownPosition = details.localPosition;
|
dragDownPosition = details.localPosition;
|
||||||
initialPhotoViewState = value;
|
initialPhotoViewState = controller.value;
|
||||||
final isZoomed =
|
final isZoomed =
|
||||||
scaleStateController.scaleState == PhotoViewScaleState.zoomedIn ||
|
scaleStateController.scaleState == PhotoViewScaleState.zoomedIn ||
|
||||||
scaleStateController.scaleState == PhotoViewScaleState.covering;
|
scaleStateController.scaleState == PhotoViewScaleState.covering;
|
||||||
@ -277,7 +304,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
void _openBottomSheet(BuildContext ctx) {
|
void _openBottomSheet(BuildContext ctx) {
|
||||||
setState(() {
|
setState(() {
|
||||||
initialScale = viewController?.scale;
|
initialScale = viewController?.scale;
|
||||||
viewController?.animateMultiple(scale: _getScaleForBottomSheet);
|
viewController?.updateMultiple(scale: _getScaleForBottomSheet);
|
||||||
showingBottomSheet = true;
|
showingBottomSheet = true;
|
||||||
previousExtent = _kBottomSheetMinimumExtent;
|
previousExtent = _kBottomSheetMinimumExtent;
|
||||||
sheetCloseNotifier = showBottomSheet(
|
sheetCloseNotifier = showBottomSheet(
|
||||||
@ -308,10 +335,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
showingBottomSheet = false;
|
showingBottomSheet = false;
|
||||||
sheetCloseNotifier = null;
|
sheetCloseNotifier = null;
|
||||||
viewController?.animateMultiple(
|
viewController?.animateMultiple(position: Offset.zero);
|
||||||
position: Offset.zero,
|
viewController?.updateMultiple(scale: initialScale);
|
||||||
scale: initialScale,
|
|
||||||
);
|
|
||||||
shouldPopOnDrag = false;
|
shouldPopOnDrag = false;
|
||||||
hasDraggedDown = null;
|
hasDraggedDown = null;
|
||||||
});
|
});
|
||||||
@ -420,10 +445,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) {
|
PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) {
|
||||||
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
||||||
final size = Size(ctx.width, ctx.height);
|
final size = Size(ctx.width, ctx.height);
|
||||||
final imageProvider = getFullImageProvider(asset, size: size);
|
|
||||||
|
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
imageProvider: imageProvider,
|
key: ValueKey(asset.heroTag),
|
||||||
|
imageProvider: getFullImageProvider(asset, size: size),
|
||||||
heroAttributes: PhotoViewHeroAttributes(tag: asset.heroTag),
|
heroAttributes: PhotoViewHeroAttributes(tag: asset.heroTag),
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
tightMode: true,
|
tightMode: true,
|
||||||
@ -446,12 +471,18 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onPop<T>(bool didPop, T? result) {
|
||||||
|
ref.read(currentAssetNotifier.notifier).dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
// 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
|
// Issue: https://github.com/flutter/flutter/issues/109037
|
||||||
// TODO: Add a custom scrum builder once the fix lands on stable
|
// TODO: Add a custom scrum builder once the fix lands on stable
|
||||||
return Scaffold(
|
return PopScope(
|
||||||
|
onPopInvokedWithResult: _onPop,
|
||||||
|
child: Scaffold(
|
||||||
backgroundColor: Colors.black.withAlpha(backgroundOpacity),
|
backgroundColor: Colors.black.withAlpha(backgroundOpacity),
|
||||||
body: PhotoViewGallery.builder(
|
body: PhotoViewGallery.builder(
|
||||||
gaplessPlayback: true,
|
gaplessPlayback: true,
|
||||||
@ -468,6 +499,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
||||||
enablePanAlways: true,
|
enablePanAlways: true,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,71 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
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/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.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_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/download_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_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/asset_viewer/bottom_sheet/location_details.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
|
|
||||||
const _kSeparator = ' • ';
|
const _kSeparator = ' • ';
|
||||||
|
|
||||||
class AssetDetailBottomSheet extends BaseBottomSheet {
|
class AssetDetailBottomSheet extends ConsumerWidget {
|
||||||
|
final DraggableScrollableController? controller;
|
||||||
|
final double initialChildSize;
|
||||||
|
|
||||||
const AssetDetailBottomSheet({
|
const AssetDetailBottomSheet({
|
||||||
super.controller,
|
this.controller,
|
||||||
super.initialChildSize,
|
this.initialChildSize = 0.35,
|
||||||
super.key,
|
super.key,
|
||||||
}) : super(
|
});
|
||||||
actions: const [],
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final asset = ref.watch(currentAssetNotifier);
|
||||||
|
final isTrashEnable = ref.watch(
|
||||||
|
serverInfoProvider.select((state) => state.serverFeatures.trash),
|
||||||
|
);
|
||||||
|
|
||||||
|
final actions = <Widget>[
|
||||||
|
const ShareActionButton(),
|
||||||
|
if (asset.hasRemote) ...[
|
||||||
|
const ShareLinkActionButton(source: ActionSource.viewer),
|
||||||
|
const ArchiveActionButton(source: ActionSource.viewer),
|
||||||
|
const FavoriteActionButton(source: ActionSource.viewer),
|
||||||
|
if (!asset.hasLocal) const DownloadActionButton(),
|
||||||
|
isTrashEnable
|
||||||
|
? const TrashActionButton(source: ActionSource.viewer)
|
||||||
|
: const DeletePermanentActionButton(source: ActionSource.viewer),
|
||||||
|
const MoveToLockFolderActionButton(
|
||||||
|
source: ActionSource.viewer,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (asset.storage == AssetState.local) ...[
|
||||||
|
const DeleteLocalActionButton(),
|
||||||
|
const UploadActionButton(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return BaseBottomSheet(
|
||||||
|
actions: actions,
|
||||||
slivers: const [_AssetDetailBottomSheet()],
|
slivers: const [_AssetDetailBottomSheet()],
|
||||||
|
controller: controller,
|
||||||
|
initialChildSize: initialChildSize,
|
||||||
minChildSize: 0.1,
|
minChildSize: 0.1,
|
||||||
maxChildSize: 1.0,
|
maxChildSize: 1.0,
|
||||||
expand: false,
|
expand: false,
|
||||||
@ -26,6 +73,7 @@ class AssetDetailBottomSheet extends BaseBottomSheet {
|
|||||||
resizeOnScroll: false,
|
resizeOnScroll: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _AssetDetailBottomSheet extends ConsumerWidget {
|
class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||||
const _AssetDetailBottomSheet();
|
const _AssetDetailBottomSheet();
|
||||||
@ -96,16 +144,16 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
// Asset Date and Time
|
// Asset Date and Time
|
||||||
_SheetTile(
|
_SheetTile(
|
||||||
title: _getDateTime(context, asset),
|
title: _getDateTime(context, asset),
|
||||||
titleStyle: context.textTheme.bodyLarge
|
titleStyle: context.textTheme.bodyLarge?.copyWith(
|
||||||
?.copyWith(fontWeight: FontWeight.w600),
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SheetLocationDetails(),
|
||||||
// Details header
|
// Details header
|
||||||
_SheetTile(
|
_SheetTile(
|
||||||
title: 'exif_bottom_sheet_details'.t(context: context),
|
title: 'exif_bottom_sheet_details'.t(context: context),
|
||||||
titleStyle: context.textTheme.labelLarge?.copyWith(
|
titleStyle: context.textTheme.labelLarge,
|
||||||
color: context.textTheme.labelLarge?.color,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
// File info
|
// File info
|
||||||
_SheetTile(
|
_SheetTile(
|
@ -0,0 +1,124 @@
|
|||||||
|
import 'package:flutter/material.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/exif.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
|
class SheetLocationDetails extends ConsumerStatefulWidget {
|
||||||
|
const SheetLocationDetails({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState createState() => _SheetLocationDetailsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
||||||
|
late BaseAsset asset;
|
||||||
|
ExifInfo? exifInfo;
|
||||||
|
MapLibreMapController? _mapController;
|
||||||
|
|
||||||
|
String? _getLocationName(ExifInfo? exifInfo) {
|
||||||
|
if (exifInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final cityName = exifInfo.city;
|
||||||
|
final stateName = exifInfo.state;
|
||||||
|
|
||||||
|
if (cityName != null && stateName != null) {
|
||||||
|
return "$cityName, $stateName";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMapCreated(MapLibreMapController controller) {
|
||||||
|
_mapController = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onExifChanged(
|
||||||
|
AsyncValue<ExifInfo?>? previous,
|
||||||
|
AsyncValue<ExifInfo?> current,
|
||||||
|
) {
|
||||||
|
asset = ref.read(currentAssetNotifier);
|
||||||
|
setState(() {
|
||||||
|
exifInfo = current.valueOrNull;
|
||||||
|
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
|
||||||
|
if (exifInfo != null && hasCoordinates) {
|
||||||
|
_mapController?.moveCamera(
|
||||||
|
CameraUpdate.newLatLng(
|
||||||
|
LatLng(exifInfo!.latitude!, exifInfo!.longitude!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
ref.listenManual(
|
||||||
|
currentAssetExifProvider,
|
||||||
|
_onExifChanged,
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
|
||||||
|
|
||||||
|
// Guard no lat/lng
|
||||||
|
if (!hasCoordinates ||
|
||||||
|
(asset is LocalAsset && !(asset as LocalAsset).hasRemote)) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final remoteId = asset is LocalAsset
|
||||||
|
? (asset as LocalAsset).remoteId
|
||||||
|
: (asset as RemoteAsset).id;
|
||||||
|
final locationName = _getLocationName(exifInfo);
|
||||||
|
final coordinates =
|
||||||
|
"${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo!.longitude!.toStringAsFixed(4)}";
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Text(
|
||||||
|
"exif_bottom_sheet_location".t(context: context),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ExifMap(
|
||||||
|
exifInfo: exifInfo!,
|
||||||
|
markerId: remoteId,
|
||||||
|
onMapCreated: _onMapCreated,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
if (locationName != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4.0),
|
||||||
|
child: Text(
|
||||||
|
locationName,
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
coordinates,
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.textTheme.labelLarge?.color?.withAlpha(150),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -89,17 +89,17 @@ class _BaseDraggableScrollableSheetState
|
|||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
if (widget.actions.isNotEmpty)
|
if (widget.actions.isNotEmpty)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 80,
|
height: 115,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
children: widget.actions,
|
children: widget.actions,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.actions.isNotEmpty) const SizedBox(height: 14),
|
if (widget.actions.isNotEmpty) ...[
|
||||||
if (widget.actions.isNotEmpty)
|
const Divider(indent: 16, endIndent: 16),
|
||||||
const Divider(indent: 20, endIndent: 20),
|
const SizedBox(height: 16),
|
||||||
if (widget.actions.isNotEmpty) const SizedBox(height: 14),
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -12,7 +12,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_f
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_buton.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/action_buttons/upload_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@ -6,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart';
|
||||||
@ -43,31 +45,36 @@ class Timeline extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SliverTimeline extends StatefulWidget {
|
class _SliverTimeline extends ConsumerStatefulWidget {
|
||||||
const _SliverTimeline();
|
const _SliverTimeline();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State createState() => _SliverTimelineState();
|
ConsumerState createState() => _SliverTimelineState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SliverTimelineState extends State<_SliverTimeline> {
|
class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
|
StreamSubscription? _reloadSubscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_reloadSubscription =
|
||||||
|
EventStream.shared.listen<TimelineReloadEvent>((_) => setState(() {}));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
|
_reloadSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext _) {
|
Widget build(BuildContext _) {
|
||||||
return Consumer(
|
|
||||||
builder: (context, ref, child) {
|
|
||||||
final asyncSegments = ref.watch(timelineSegmentProvider);
|
final asyncSegments = ref.watch(timelineSegmentProvider);
|
||||||
final maxHeight =
|
final maxHeight =
|
||||||
ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
|
ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
|
||||||
final isMultiSelectEnabled =
|
|
||||||
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
|
||||||
return asyncSegments.widgetWhen(
|
return asyncSegments.widgetWhen(
|
||||||
onData: (segments) {
|
onData: (segments) {
|
||||||
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
|
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
|
||||||
@ -83,21 +90,16 @@ class _SliverTimelineState extends State<_SliverTimeline> {
|
|||||||
layoutSegments: segments,
|
layoutSegments: segments,
|
||||||
timelineHeight: maxHeight,
|
timelineHeight: maxHeight,
|
||||||
topPadding: totalAppBarHeight + 10,
|
topPadding: totalAppBarHeight + 10,
|
||||||
bottomPadding:
|
bottomPadding: context.padding.bottom + scrubberBottomPadding,
|
||||||
context.padding.bottom + scrubberBottomPadding,
|
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
primary: true,
|
primary: true,
|
||||||
cacheExtent: maxHeight * 2,
|
cacheExtent: maxHeight * 2,
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAnimatedOpacity(
|
const ImmichSliverAppBar(
|
||||||
duration: Durations.medium1,
|
|
||||||
opacity: isMultiSelectEnabled ? 0 : 1,
|
|
||||||
sliver: const ImmichSliverAppBar(
|
|
||||||
floating: true,
|
floating: true,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
snap: false,
|
snap: false,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
_SliverSegmentedList(
|
_SliverSegmentedList(
|
||||||
segments: segments,
|
segments: segments,
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
@ -121,18 +123,42 @@ class _SliverTimelineState extends State<_SliverTimeline> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isMultiSelectEnabled) ...[
|
Consumer(
|
||||||
const Positioned(
|
builder: (_, consumerRef, child) {
|
||||||
|
final isMultiSelectEnabled = consumerRef.watch(
|
||||||
|
multiSelectProvider.select(
|
||||||
|
(s) => s.isEnabled,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMultiSelectEnabled) {
|
||||||
|
return child!;
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
child: const Positioned(
|
||||||
top: 60,
|
top: 60,
|
||||||
left: 25,
|
left: 25,
|
||||||
child: _MultiSelectStatusButton(),
|
child: _MultiSelectStatusButton(),
|
||||||
),
|
),
|
||||||
const HomeBottomAppBar(),
|
),
|
||||||
],
|
Consumer(
|
||||||
],
|
builder: (_, consumerRef, child) {
|
||||||
|
final isMultiSelectEnabled = consumerRef.watch(
|
||||||
|
multiSelectProvider.select(
|
||||||
|
(s) => s.isEnabled,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isMultiSelectEnabled) {
|
||||||
|
return child!;
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
|
child: const HomeBottomAppBar(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/constants/enums.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/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/services/action.service.dart';
|
import 'package:immich_mobile/services/action.service.dart';
|
||||||
@ -55,7 +56,7 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
final Set<BaseAsset> assets = switch (source) {
|
final Set<BaseAsset> assets = switch (source) {
|
||||||
ActionSource.timeline =>
|
ActionSource.timeline =>
|
||||||
ref.read(multiSelectProvider.select((s) => s.selectedAssets)),
|
ref.read(multiSelectProvider.select((s) => s.selectedAssets)),
|
||||||
ActionSource.viewer => {},
|
ActionSource.viewer => {ref.read(currentAssetNotifier)},
|
||||||
};
|
};
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
|
@ -3,9 +3,13 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
|
|
||||||
final currentAssetNotifier =
|
final currentAssetNotifier =
|
||||||
NotifierProvider<CurrentAssetNotifier, BaseAsset>(CurrentAssetNotifier.new);
|
AutoDisposeNotifierProvider<CurrentAssetNotifier, BaseAsset>(
|
||||||
|
CurrentAssetNotifier.new,
|
||||||
|
);
|
||||||
|
|
||||||
|
class CurrentAssetNotifier extends AutoDisposeNotifier<BaseAsset> {
|
||||||
|
KeepAliveLink? _keepAliveLink;
|
||||||
|
|
||||||
class CurrentAssetNotifier extends Notifier<BaseAsset> {
|
|
||||||
@override
|
@override
|
||||||
BaseAsset build() {
|
BaseAsset build() {
|
||||||
throw UnimplementedError(
|
throw UnimplementedError(
|
||||||
@ -14,11 +18,17 @@ class CurrentAssetNotifier extends Notifier<BaseAsset> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setAsset(BaseAsset asset) {
|
void setAsset(BaseAsset asset) {
|
||||||
|
_keepAliveLink?.close();
|
||||||
state = asset;
|
state = asset;
|
||||||
|
_keepAliveLink = ref.keepAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_keepAliveLink?.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final currentAssetExifProvider = FutureProvider(
|
final currentAssetExifProvider = FutureProvider.autoDispose(
|
||||||
(ref) {
|
(ref) {
|
||||||
final currentAsset = ref.watch(currentAssetNotifier);
|
final currentAsset = ref.watch(currentAssetNotifier);
|
||||||
return ref.watch(assetServiceProvider).getExif(currentAsset);
|
return ref.watch(assetServiceProvider).getExif(currentAsset);
|
||||||
|
@ -9,11 +9,13 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
class ExifMap extends StatelessWidget {
|
class ExifMap extends StatelessWidget {
|
||||||
final ExifInfo exifInfo;
|
final ExifInfo exifInfo;
|
||||||
final String? markerId;
|
final String? markerId;
|
||||||
|
final MapCreatedCallback? onMapCreated;
|
||||||
|
|
||||||
const ExifMap({
|
const ExifMap({
|
||||||
super.key,
|
super.key,
|
||||||
required this.exifInfo,
|
required this.exifInfo,
|
||||||
this.markerId = 'marker',
|
this.markerId = 'marker',
|
||||||
|
this.onMapCreated,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -82,6 +84,7 @@ class ExifMap extends StatelessWidget {
|
|||||||
debugPrint('Opening Map Uri: $uri');
|
debugPrint('Opening Map Uri: $uri');
|
||||||
launchUrl(uri);
|
launchUrl(uri);
|
||||||
},
|
},
|
||||||
|
onCreated: onMapCreated,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -10,6 +10,7 @@ import 'package:immich_mobile/providers/background_sync.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
||||||
@ -39,8 +40,13 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||||
|
final isMultiSelectEnabled =
|
||||||
|
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAnimatedOpacity(
|
||||||
|
duration: Durations.medium1,
|
||||||
|
opacity: isMultiSelectEnabled ? 0 : 1,
|
||||||
|
sliver: SliverAppBar(
|
||||||
floating: floating,
|
floating: floating,
|
||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
snap: snap,
|
snap: snap,
|
||||||
@ -97,6 +103,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
child: _ProfileIndicator(),
|
child: _ProfileIndicator(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||||
import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
|
import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
|
||||||
@ -24,6 +25,7 @@ class MapThumbnail extends HookConsumerWidget {
|
|||||||
final double width;
|
final double width;
|
||||||
final ThemeMode? themeMode;
|
final ThemeMode? themeMode;
|
||||||
final bool showAttribution;
|
final bool showAttribution;
|
||||||
|
final MapCreatedCallback? onCreated;
|
||||||
|
|
||||||
const MapThumbnail({
|
const MapThumbnail({
|
||||||
super.key,
|
super.key,
|
||||||
@ -36,16 +38,19 @@ class MapThumbnail extends HookConsumerWidget {
|
|||||||
this.showMarkerPin = false,
|
this.showMarkerPin = false,
|
||||||
this.themeMode,
|
this.themeMode,
|
||||||
this.showAttribution = true,
|
this.showAttribution = true,
|
||||||
|
this.onCreated,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final offsettedCentre = LatLng(centre.latitude + 0.002, centre.longitude);
|
final offsettedCentre = LatLng(centre.latitude + 0.002, centre.longitude);
|
||||||
final controller = useRef<MapLibreMapController?>(null);
|
final controller = useRef<MapLibreMapController?>(null);
|
||||||
|
final styleLoaded = useState(false);
|
||||||
final position = useValueNotifier<Point<num>?>(null);
|
final position = useValueNotifier<Point<num>?>(null);
|
||||||
|
|
||||||
Future<void> onMapCreated(MapLibreMapController mapController) async {
|
Future<void> onMapCreated(MapLibreMapController mapController) async {
|
||||||
controller.value = mapController;
|
controller.value = mapController;
|
||||||
|
styleLoaded.value = false;
|
||||||
if (assetMarkerRemoteId != null) {
|
if (assetMarkerRemoteId != null) {
|
||||||
// The iOS impl returns wrong toScreenLocation without the delay
|
// The iOS impl returns wrong toScreenLocation without the delay
|
||||||
Future.delayed(
|
Future.delayed(
|
||||||
@ -54,17 +59,26 @@ class MapThumbnail extends HookConsumerWidget {
|
|||||||
position.value = await mapController.toScreenLocation(centre),
|
position.value = await mapController.toScreenLocation(centre),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
onCreated?.call(mapController);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onStyleLoaded() async {
|
Future<void> onStyleLoaded() async {
|
||||||
if (showMarkerPin && controller.value != null) {
|
if (showMarkerPin && controller.value != null) {
|
||||||
await controller.value?.addMarkerAtLatLng(centre);
|
await controller.value?.addMarkerAtLatLng(centre);
|
||||||
}
|
}
|
||||||
|
styleLoaded.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MapThemeOverride(
|
return MapThemeOverride(
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
mapBuilder: (style) => SizedBox(
|
mapBuilder: (style) => AnimatedContainer(
|
||||||
|
duration: Durations.medium2,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
foregroundDecoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.inverseSurface
|
||||||
|
.withAlpha(styleLoaded.value ? 0 : 200),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
|
),
|
||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
|
@ -660,7 +660,7 @@ typedef PhotoViewImageTapDownCallback = Function(
|
|||||||
typedef PhotoViewImageDragStartCallback = Function(
|
typedef PhotoViewImageDragStartCallback = Function(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
DragStartDetails details,
|
DragStartDetails details,
|
||||||
PhotoViewControllerValue controllerValue,
|
PhotoViewControllerBase controllerValue,
|
||||||
PhotoViewScaleStateController scaleStateController,
|
PhotoViewScaleStateController scaleStateController,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -271,7 +271,7 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
|||||||
|
|
||||||
final PhotoView photoView = isCustomChild
|
final PhotoView photoView = isCustomChild
|
||||||
? PhotoView.customChild(
|
? PhotoView.customChild(
|
||||||
key: ObjectKey(index),
|
key: pageOption.key ?? ObjectKey(index),
|
||||||
childSize: pageOption.childSize,
|
childSize: pageOption.childSize,
|
||||||
backgroundDecoration: widget.backgroundDecoration,
|
backgroundDecoration: widget.backgroundDecoration,
|
||||||
wantKeepAlive: widget.wantKeepAlive,
|
wantKeepAlive: widget.wantKeepAlive,
|
||||||
@ -304,7 +304,7 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
|||||||
child: pageOption.child,
|
child: pageOption.child,
|
||||||
)
|
)
|
||||||
: PhotoView(
|
: PhotoView(
|
||||||
key: ObjectKey(index),
|
key: pageOption.key ?? ObjectKey(index),
|
||||||
index: index,
|
index: index,
|
||||||
imageProvider: pageOption.imageProvider,
|
imageProvider: pageOption.imageProvider,
|
||||||
loadingBuilder: widget.loadingBuilder,
|
loadingBuilder: widget.loadingBuilder,
|
||||||
@ -363,7 +363,7 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
|||||||
///
|
///
|
||||||
class PhotoViewGalleryPageOptions {
|
class PhotoViewGalleryPageOptions {
|
||||||
PhotoViewGalleryPageOptions({
|
PhotoViewGalleryPageOptions({
|
||||||
Key? key,
|
this.key,
|
||||||
required this.imageProvider,
|
required this.imageProvider,
|
||||||
this.heroAttributes,
|
this.heroAttributes,
|
||||||
this.semanticLabel,
|
this.semanticLabel,
|
||||||
@ -392,6 +392,7 @@ class PhotoViewGalleryPageOptions {
|
|||||||
assert(imageProvider != null);
|
assert(imageProvider != null);
|
||||||
|
|
||||||
const PhotoViewGalleryPageOptions.customChild({
|
const PhotoViewGalleryPageOptions.customChild({
|
||||||
|
this.key,
|
||||||
required this.child,
|
required this.child,
|
||||||
this.childSize,
|
this.childSize,
|
||||||
this.semanticLabel,
|
this.semanticLabel,
|
||||||
@ -418,6 +419,8 @@ class PhotoViewGalleryPageOptions {
|
|||||||
}) : errorBuilder = null,
|
}) : errorBuilder = null,
|
||||||
imageProvider = null;
|
imageProvider = null;
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
/// Mirror to [PhotoView.imageProvider]
|
/// Mirror to [PhotoView.imageProvider]
|
||||||
final ImageProvider? imageProvider;
|
final ImageProvider? imageProvider;
|
||||||
|
|
||||||
|
@ -416,7 +416,7 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
? (details) => widget.onDragStart!(
|
? (details) => widget.onDragStart!(
|
||||||
context,
|
context,
|
||||||
details,
|
details,
|
||||||
widget.controller.value,
|
widget.controller,
|
||||||
widget.scaleStateController,
|
widget.scaleStateController,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user