mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
feat(mobile): add read only mode (#19368)
* feat(mobile): Add Kid (Readonly) Mode toggle This commit introduces a "Kid (Readonly) Mode" feature. - Adds a `KidModeProvider` to manage the state of Kid Mode. - Implements a `KidModeCheckbox` widget in the app bar dialog to toggle Kid Mode. - When Kid Mode is enabled, - Disables selecting the multigrid & the bottom bar - Removes the top bar from view Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com> Reverts the changes to devtools_options.yaml file Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com> refactor: replace Kid Mode with Readonly Mode This commit replaces the "Kid Mode" feature with a more generic "Readonly Mode". - Renamed `KidModeProvider` to `ReadonlyModeProvider`. - Readonly Mode state is now persisted in app settings. - Added a new app setting `allowUserAvatarOverride` to toggle read-only mode. - Updated translations. - Added a message in the app bar dialog indicating when read-only mode is active. Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com> Address comments - - Removes the `allowUserAvatarOverride` setting. - Hides the bottom gallery bar when read-only mode is enabled. - Adds an icon on the main app bar when read-only mode is enabled with a snackbar. Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com> Update to snackbar - When toggling readonly mode from either the settings or the app bar, a snackbar notification will now appear. - The readonly mode message in the profile drawer has been restyled. - The upload button in the app bar is now hidden when readonly mode is enabled. Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com> Removes clearing of snackbar Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com> Address Comments - Consolidated snackbar messages for enabling/disabling readonly mode. - Ensured the "Select All" icon in asset group titles is hidden in readonly mode. Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com> Adds in the missing translation keys for readonly_mode Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com> Fix translation Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com> Fix check failure for BorderRadius Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com> Changes: - Adjusted AppBar background color in readonly mode. - Removes cross-out pencil icon button in favor of above. - Hides the "Edit" icon next to date/time, disable description and onTap for people and location when readonly mode is enabled. Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com> Address comments from Alex - Moved readonly mode check to `GalleryAppBar` to hide the entire `TopControlAppBar` when readonly mode is enabled. - Changed `toggleReadonlyMode` in `ImmichAppBar` to directly toggle the state. Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com> migrate readonly mode to new beta timeline remove readonly mode from legacy UI only show readonly functionality when on beta timeline simplify selection icon update generated provider chore: more formatting * fix: bad merge * chore: use Notifier for readonlyModeProvider * fix: drag select now honors readonly mode * fix: disable asset bottom sheet in readonly * fix: disable editing user icon when in readonly * chore: remove generated file * fix: disable tabs instead entire tab bar This solves the issues with the scrubber * chore: remove unneeded import * chore: lint * remove unused condition in bottomsheet --------- Co-authored-by: Brandon Wees <brandonwees@gmail.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
662d44536e
commit
8853079c54
16 changed files with 187 additions and 33 deletions
|
|
@ -396,6 +396,8 @@
|
||||||
"advanced_settings_prefer_remote_title": "Prefer remote images",
|
"advanced_settings_prefer_remote_title": "Prefer remote images",
|
||||||
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
|
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
|
||||||
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
||||||
|
"advanced_settings_readonly_mode_subtitle": "Enables the read-only mode where the photos can be only viewed, things like selecting multiple images, sharing, casting, delete are all disabled. Enable/Disable read-only via user avatar from the main screen",
|
||||||
|
"advanced_settings_readonly_mode_title": "Read-only Mode",
|
||||||
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
|
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
|
||||||
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
|
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
|
||||||
"advanced_settings_sync_remote_deletions_subtitle": "Automatically delete or restore an asset on this device when that action is taken on the web",
|
"advanced_settings_sync_remote_deletions_subtitle": "Automatically delete or restore an asset on this device when that action is taken on the web",
|
||||||
|
|
@ -1516,6 +1518,7 @@
|
||||||
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
|
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
|
||||||
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
||||||
"profile_drawer_github": "GitHub",
|
"profile_drawer_github": "GitHub",
|
||||||
|
"profile_drawer_readonly_mode": "Read-only mode enabled. Double-tap the user avatar icon to exit.",
|
||||||
"profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
|
"profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
|
||||||
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
|
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
|
||||||
"profile_image_of_user": "Profile image of {user}",
|
"profile_image_of_user": "Profile image of {user}",
|
||||||
|
|
@ -1561,6 +1564,8 @@
|
||||||
"rating_description": "Display the EXIF rating in the info panel",
|
"rating_description": "Display the EXIF rating in the info panel",
|
||||||
"reaction_options": "Reaction options",
|
"reaction_options": "Reaction options",
|
||||||
"read_changelog": "Read Changelog",
|
"read_changelog": "Read Changelog",
|
||||||
|
"readonly_mode_disabled": "Read-only mode disabled",
|
||||||
|
"readonly_mode_enabled": "Read-only mode enabled",
|
||||||
"reassign": "Reassign",
|
"reassign": "Reassign",
|
||||||
"reassigned_assets_to_existing_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to {name, select, null {an existing person} other {{name}}}",
|
"reassigned_assets_to_existing_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to {name, select, null {an existing person} other {{name}}}",
|
||||||
"reassigned_assets_to_new_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to a new person",
|
"reassigned_assets_to_new_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to a new person",
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,9 @@ enum StoreKey<T> {
|
||||||
loadOriginalVideo<bool>._(136),
|
loadOriginalVideo<bool>._(136),
|
||||||
manageLocalMediaAndroid<bool>._(137),
|
manageLocalMediaAndroid<bool>._(137),
|
||||||
|
|
||||||
|
// Read-only Mode settings
|
||||||
|
readonlyModeEnabled<bool>._(138),
|
||||||
|
|
||||||
// Experimental stuff
|
// Experimental stuff
|
||||||
photoManagerCustomFilter<bool>._(1000),
|
photoManagerCustomFilter<bool>._(1000),
|
||||||
betaPromptShown<bool>._(1001),
|
betaPromptShown<bool>._(1001),
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/utils/migration.dart';
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
|
|
@ -75,6 +76,7 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||||
await ref.read(backgroundSyncProvider).cancel();
|
await ref.read(backgroundSyncProvider).cancel();
|
||||||
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
|
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
|
||||||
ref.read(websocketProvider.notifier).startListeningToOldEvents();
|
ref.read(websocketProvider.notifier).startListeningToOldEvents();
|
||||||
|
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
|
||||||
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
||||||
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||||
await ref.read(driftBackgroundUploadFgService).disableUploadService();
|
await ref.read(driftBackgroundUploadFgService).disableUploadService();
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
@ -54,6 +55,7 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isScreenLandscape = context.orientation == Orientation.landscape;
|
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||||
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
|
|
||||||
final navigationDestinations = [
|
final navigationDestinations = [
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
|
|
@ -65,16 +67,19 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
|
||||||
label: 'search'.tr(),
|
label: 'search'.tr(),
|
||||||
icon: const Icon(Icons.search_rounded),
|
icon: const Icon(Icons.search_rounded),
|
||||||
selectedIcon: Icon(Icons.search, color: context.primaryColor),
|
selectedIcon: Icon(Icons.search, color: context.primaryColor),
|
||||||
|
enabled: !isReadonlyModeEnabled,
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'albums'.tr(),
|
label: 'albums'.tr(),
|
||||||
icon: const Icon(Icons.photo_album_outlined),
|
icon: const Icon(Icons.photo_album_outlined),
|
||||||
selectedIcon: Icon(Icons.photo_album_rounded, color: context.primaryColor),
|
selectedIcon: Icon(Icons.photo_album_rounded, color: context.primaryColor),
|
||||||
|
enabled: !isReadonlyModeEnabled,
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'library'.tr(),
|
label: 'library'.tr(),
|
||||||
icon: const Icon(Icons.space_dashboard_outlined),
|
icon: const Icon(Icons.space_dashboard_outlined),
|
||||||
selectedIcon: Icon(Icons.space_dashboard_rounded, color: context.primaryColor),
|
selectedIcon: Icon(Icons.space_dashboard_rounded, color: context.primaryColor),
|
||||||
|
enabled: !isReadonlyModeEnabled,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provi
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||||
import 'package:immich_mobile/providers/cast.provider.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/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.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.dart';
|
||||||
|
|
@ -308,7 +309,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
bottomSheetController.jumpTo((centre + distanceToOrigin) / ctx.height);
|
bottomSheetController.jumpTo((centre + distanceToOrigin) / ctx.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distanceToOrigin > openThreshold && !showingBottomSheet) {
|
if (distanceToOrigin > openThreshold && !showingBottomSheet && !ref.read(readonlyModeProvider)) {
|
||||||
_openBottomSheet(ctx);
|
_openBottomSheet(ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_acti
|
||||||
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/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.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/infrastructure/readonly_mode.provider.dart';
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
|
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
|
||||||
|
|
@ -26,6 +27,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||||
final isSheetOpen = ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
final isSheetOpen = ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
||||||
|
|
@ -60,7 +62,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||||
duration: Durations.short2,
|
duration: Durations.short2,
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
duration: Durations.short4,
|
duration: Durations.short4,
|
||||||
child: isSheetOpen
|
child: isSheetOpen || isReadonlyModeEnabled
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: Theme(
|
: Theme(
|
||||||
data: context.themeData.copyWith(
|
data: context.themeData.copyWith(
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ 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/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
import 'package:immich_mobile/providers/cast.provider.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/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/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
@ -34,6 +35,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
|
|
||||||
final previousRouteName = ref.watch(previousRouteNameProvider);
|
final previousRouteName = ref.watch(previousRouteNameProvider);
|
||||||
final showViewInTimelineButton =
|
final showViewInTimelineButton =
|
||||||
|
|
@ -94,7 +96,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
iconTheme: const IconThemeData(size: 22, color: Colors.white),
|
iconTheme: const IconThemeData(size: 22, color: Colors.white),
|
||||||
actionsIconTheme: const IconThemeData(size: 22, color: Colors.white),
|
actionsIconTheme: const IconThemeData(size: 22, color: Colors.white),
|
||||||
shape: const Border(),
|
shape: const Border(),
|
||||||
actions: isShowingSheet
|
actions: isShowingSheet || isReadonlyModeEnabled
|
||||||
? null
|
? null
|
||||||
: isInLockedView
|
: isInLockedView
|
||||||
? lockedViewActions
|
? lockedViewActions
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region
|
||||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
|
@ -190,11 +191,12 @@ class _AssetTileWidget extends ConsumerWidget {
|
||||||
|
|
||||||
final lockSelection = _getLockSelectionStatus(ref);
|
final lockSelection = _getLockSelectionStatus(ref);
|
||||||
final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator));
|
final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator));
|
||||||
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
|
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => lockSelection ? null : _handleOnTap(context, ref, assetIndex, asset, heroOffset),
|
onTap: () => lockSelection ? null : _handleOnTap(context, ref, assetIndex, asset, heroOffset),
|
||||||
onLongPress: () => lockSelection ? null : _handleOnLongPress(ref, asset),
|
onLongPress: () => lockSelection || isReadonlyModeEnabled ? null : _handleOnLongPress(ref, asset),
|
||||||
child: ThumbnailTile(
|
child: ThumbnailTile(
|
||||||
asset,
|
asset,
|
||||||
lockSelection: lockSelection,
|
lockSelection: lockSelection,
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,10 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
class TimelineHeader extends StatelessWidget {
|
class TimelineHeader extends HookConsumerWidget {
|
||||||
final Bucket bucket;
|
final Bucket bucket;
|
||||||
final HeaderType header;
|
final HeaderType header;
|
||||||
final double height;
|
final double height;
|
||||||
|
|
@ -36,13 +37,12 @@ class TimelineHeader extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
if (bucket is! TimeBucket || header == HeaderType.none) {
|
if (bucket is! TimeBucket || header == HeaderType.none) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final date = (bucket as TimeBucket).date;
|
final date = (bucket as TimeBucket).date;
|
||||||
|
|
||||||
final isMonthHeader = header == HeaderType.month || header == HeaderType.monthAndDay;
|
final isMonthHeader = header == HeaderType.month || header == HeaderType.monthAndDay;
|
||||||
final isDayHeader = header == HeaderType.day || header == HeaderType.monthAndDay;
|
final isDayHeader = header == HeaderType.day || header == HeaderType.monthAndDay;
|
||||||
|
|
||||||
|
|
@ -98,16 +98,19 @@ class _BulkSelectIconButton extends ConsumerWidget {
|
||||||
bucketAssets = <BaseAsset>[];
|
bucketAssets = <BaseAsset>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
final isAllSelected = ref.watch(bucketSelectionProvider(bucketAssets));
|
final isAllSelected = ref.watch(bucketSelectionProvider(bucketAssets));
|
||||||
|
|
||||||
return IconButton(
|
return isReadonlyModeEnabled
|
||||||
onPressed: () {
|
? const SizedBox.shrink()
|
||||||
ref.read(multiSelectProvider.notifier).toggleBucketSelection(assetOffset, bucket.assetCount);
|
: IconButton(
|
||||||
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
onPressed: () {
|
||||||
},
|
ref.read(multiSelectProvider.notifier).toggleBucketSelection(assetOffset, bucket.assetCount);
|
||||||
icon: isAllSelected
|
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||||
? Icon(Icons.check_circle_rounded, size: 26, color: context.primaryColor)
|
},
|
||||||
: Icon(Icons.check_circle_outline_rounded, size: 26, color: context.colorScheme.onSurfaceSecondary),
|
icon: isAllSelected
|
||||||
);
|
? Icon(Icons.check_circle_rounded, size: 26, color: context.primaryColor)
|
||||||
|
: Icon(Icons.check_circle_outline_rounded, size: 26, color: context.colorScheme.onSurfaceSecondary),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.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/presentation/widgets/timeline/timeline.state.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.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';
|
||||||
|
|
@ -256,6 +257,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||||
final maxHeight = ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
|
final maxHeight = ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
|
||||||
final isSelectionMode = ref.watch(multiSelectProvider.select((s) => s.forceEnable));
|
final isSelectionMode = ref.watch(multiSelectProvider.select((s) => s.forceEnable));
|
||||||
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||||
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: !isMultiSelectEnabled,
|
canPop: !isMultiSelectEnabled,
|
||||||
|
|
@ -342,9 +344,9 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
child: TimelineDragRegion(
|
child: TimelineDragRegion(
|
||||||
onStart: _setDragStartIndex,
|
onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null,
|
||||||
onAssetEnter: _handleDragAssetEnter,
|
onAssetEnter: _handleDragAssetEnter,
|
||||||
onEnd: _stopDrag,
|
onEnd: !isReadonlyModeEnabled ? _stopDrag : null,
|
||||||
onScroll: _dragScroll,
|
onScroll: _dragScroll,
|
||||||
onScrollStart: () {
|
onScrollStart: () {
|
||||||
// Minimize the bottom sheet when drag selection starts
|
// Minimize the bottom sheet when drag selection starts
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
|
|
||||||
|
class ReadOnlyModeNotifier extends Notifier<bool> {
|
||||||
|
late AppSettingsService _appSettingService;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool build() {
|
||||||
|
_appSettingService = ref.read(appSettingsServiceProvider);
|
||||||
|
final readonlyMode = _appSettingService.getSetting(AppSettingsEnum.readonlyModeEnabled);
|
||||||
|
return readonlyMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMode(bool value) {
|
||||||
|
_appSettingService.setSetting(AppSettingsEnum.readonlyModeEnabled, value);
|
||||||
|
state = value;
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
ref.read(appRouterProvider).navigate(const MainTimelineRoute());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setReadonlyMode(bool isEnabled) {
|
||||||
|
state = isEnabled;
|
||||||
|
setMode(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleReadonlyMode() {
|
||||||
|
state = !state;
|
||||||
|
setMode(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final readonlyModeProvider = NotifierProvider<ReadOnlyModeNotifier, bool>(() => ReadOnlyModeNotifier());
|
||||||
|
|
@ -49,7 +49,8 @@ enum AppSettingsEnum<T> {
|
||||||
betaTimeline<bool>(StoreKey.betaTimeline, null, false),
|
betaTimeline<bool>(StoreKey.betaTimeline, null, false),
|
||||||
enableBackup<bool>(StoreKey.enableBackup, null, false),
|
enableBackup<bool>(StoreKey.enableBackup, null, false),
|
||||||
useCellularForUploadVideos<bool>(StoreKey.useWifiForUploadVideos, null, false),
|
useCellularForUploadVideos<bool>(StoreKey.useWifiForUploadVideos, null, false),
|
||||||
useCellularForUploadPhotos<bool>(StoreKey.useWifiForUploadPhotos, null, false);
|
useCellularForUploadPhotos<bool>(StoreKey.useWifiForUploadPhotos, null, false),
|
||||||
|
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false);
|
||||||
|
|
||||||
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/locale_provider.dart';
|
import 'package:immich_mobile/providers/locale_provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart';
|
import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart';
|
||||||
|
|
@ -33,6 +34,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||||
final horizontalPadding = isHorizontal ? 100.0 : 20.0;
|
final horizontalPadding = isHorizontal ? 100.0 : 20.0;
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
final isLoggingOut = useState(false);
|
final isLoggingOut = useState(false);
|
||||||
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
ref.read(backupProvider.notifier).updateDiskInfo();
|
ref.read(backupProvider.notifier).updateDiskInfo();
|
||||||
|
|
@ -214,6 +216,25 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildReadonlyMessage() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 10.0, right: 10.0),
|
||||||
|
child: ListTile(
|
||||||
|
dense: true,
|
||||||
|
visualDensity: VisualDensity.standard,
|
||||||
|
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||||
|
minLeadingWidth: 20,
|
||||||
|
tileColor: theme.primaryColor.withAlpha(80),
|
||||||
|
title: Text(
|
||||||
|
"profile_drawer_readonly_mode",
|
||||||
|
style: theme.textTheme.labelLarge?.copyWith(color: theme.textTheme.labelLarge?.color?.withAlpha(250)),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Dismissible(
|
return Dismissible(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
direction: DismissDirection.down,
|
direction: DismissDirection.down,
|
||||||
|
|
@ -238,6 +259,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||||
const AppBarProfileInfoBox(),
|
const AppBarProfileInfoBox(),
|
||||||
buildStorageInformation(),
|
buildStorageInformation(),
|
||||||
const AppBarServerInfo(),
|
const AppBarServerInfo(),
|
||||||
|
if (isReadonlyModeEnabled) buildReadonlyMessage(),
|
||||||
buildAppLogButton(),
|
buildAppLogButton(),
|
||||||
buildSettingButton(),
|
buildSettingButton(),
|
||||||
buildSignOutButton(),
|
buildSignOutButton(),
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
|
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:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.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/upload_profile_image.provider.dart';
|
import 'package:immich_mobile/providers/upload_profile_image.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
@ -17,6 +20,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final authState = ref.watch(authProvider);
|
final authState = ref.watch(authProvider);
|
||||||
final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status;
|
final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status;
|
||||||
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
|
|
||||||
buildUserProfileImage() {
|
buildUserProfileImage() {
|
||||||
|
|
@ -55,6 +59,25 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void toggleReadonlyMode() {
|
||||||
|
// read only mode is only supported int he beta experience
|
||||||
|
// TODO: remove this check when the beta UI goes stable
|
||||||
|
if (!Store.isBetaTimelineEnabled) return;
|
||||||
|
|
||||||
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
|
ref.read(readonlyModeProvider.notifier).toggleReadonlyMode();
|
||||||
|
|
||||||
|
context.scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
content: Text(
|
||||||
|
(isReadonlyModeEnabled ? "readonly_mode_disabled" : "readonly_mode_enabled").tr(),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|
@ -67,23 +90,25 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
||||||
minLeadingWidth: 50,
|
minLeadingWidth: 50,
|
||||||
leading: GestureDetector(
|
leading: GestureDetector(
|
||||||
onTap: pickUserProfileImage,
|
onTap: pickUserProfileImage,
|
||||||
|
onDoubleTap: toggleReadonlyMode,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
buildUserProfileImage(),
|
buildUserProfileImage(),
|
||||||
Positioned(
|
if (!isReadonlyModeEnabled)
|
||||||
bottom: -5,
|
Positioned(
|
||||||
right: -8,
|
bottom: -5,
|
||||||
child: Material(
|
right: -8,
|
||||||
color: context.colorScheme.surfaceContainerHighest,
|
child: Material(
|
||||||
elevation: 3,
|
color: context.colorScheme.surfaceContainerHighest,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))),
|
elevation: 3,
|
||||||
child: Padding(
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))),
|
||||||
padding: const EdgeInsets.all(5.0),
|
child: Padding(
|
||||||
child: Icon(Icons.camera_alt_outlined, color: context.primaryColor, size: 14),
|
padding: const EdgeInsets.all(5.0),
|
||||||
|
child: Icon(Icons.camera_alt_outlined, color: context.primaryColor, size: 14),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.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/sync_status.provider.dart';
|
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
@ -42,6 +43,7 @@ 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 isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||||
|
|
||||||
return SliverAnimatedOpacity(
|
return SliverAnimatedOpacity(
|
||||||
|
|
@ -57,7 +59,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
title: title ?? const _ImmichLogoWithText(),
|
title: title ?? const _ImmichLogoWithText(),
|
||||||
actions: [
|
actions: [
|
||||||
if (isCasting)
|
if (isCasting && !isReadonlyModeEnabled)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 12),
|
padding: const EdgeInsets.only(right: 12),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
|
|
@ -70,12 +72,13 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
||||||
const _SyncStatusIndicator(),
|
const _SyncStatusIndicator(),
|
||||||
if (actions != null)
|
if (actions != null)
|
||||||
...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)),
|
...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)),
|
||||||
if (kDebugMode || kProfileMode)
|
if ((kDebugMode || kProfileMode) && !isReadonlyModeEnabled)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.science_rounded),
|
icon: const Icon(Icons.science_rounded),
|
||||||
onPressed: () => context.pushRoute(const FeatInDevRoute()),
|
onPressed: () => context.pushRoute(const FeatInDevRoute()),
|
||||||
),
|
),
|
||||||
if (showUploadButton) const Padding(padding: EdgeInsets.only(right: 20), child: _BackupIndicator()),
|
if (showUploadButton && !isReadonlyModeEnabled)
|
||||||
|
const Padding(padding: EdgeInsets.only(right: 20), child: _BackupIndicator()),
|
||||||
const Padding(padding: EdgeInsets.only(right: 20), child: _ProfileIndicator()),
|
const Padding(padding: EdgeInsets.only(right: 20), child: _ProfileIndicator()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -137,8 +140,24 @@ class _ProfileIndicator extends ConsumerWidget {
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
const widgetSize = 30.0;
|
const widgetSize = 30.0;
|
||||||
|
|
||||||
|
void toggleReadonlyMode() {
|
||||||
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
|
ref.read(readonlyModeProvider.notifier).toggleReadonlyMode();
|
||||||
|
|
||||||
|
context.scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
content: Text(
|
||||||
|
(isReadonlyModeEnabled ? "readonly_mode_disabled" : "readonly_mode_enabled").tr(),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()),
|
onTap: () => showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()),
|
||||||
|
onDoubleTap: () => toggleReadonlyMode(),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
child: Badge(
|
child: Badge(
|
||||||
label: Container(
|
label: Container(
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||||
|
|
@ -31,6 +34,7 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||||
final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage);
|
final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage);
|
||||||
final allowSelfSignedSSLCert = useAppSettingsState(AppSettingsEnum.allowSelfSignedSSLCert);
|
final allowSelfSignedSSLCert = useAppSettingsState(AppSettingsEnum.allowSelfSignedSSLCert);
|
||||||
final useAlternatePMFilter = useAppSettingsState(AppSettingsEnum.photoManagerCustomFilter);
|
final useAlternatePMFilter = useAppSettingsState(AppSettingsEnum.photoManagerCustomFilter);
|
||||||
|
final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled);
|
||||||
|
|
||||||
final logLevel = Level.LEVELS[levelId.value].name;
|
final logLevel = Level.LEVELS[levelId.value].name;
|
||||||
|
|
||||||
|
|
@ -102,6 +106,26 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||||
title: "advanced_settings_enable_alternate_media_filter_title".tr(),
|
title: "advanced_settings_enable_alternate_media_filter_title".tr(),
|
||||||
subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(),
|
subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(),
|
||||||
),
|
),
|
||||||
|
// TODO: Remove this check when beta timeline goes stable
|
||||||
|
if (Store.isBetaTimelineEnabled)
|
||||||
|
SettingsSwitchListTile(
|
||||||
|
valueNotifier: readonlyModeEnabled,
|
||||||
|
title: "advanced_settings_readonly_mode_title".tr(),
|
||||||
|
subtitle: "advanced_settings_readonly_mode_subtitle".tr(),
|
||||||
|
onChanged: (value) {
|
||||||
|
readonlyModeEnabled.value = value;
|
||||||
|
ref.read(readonlyModeProvider.notifier).setReadonlyMode(value);
|
||||||
|
context.scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
content: Text(
|
||||||
|
(value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return SettingsSubPageScaffold(settings: advancedSettings);
|
return SettingsSubPageScaffold(settings: advancedSettings);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue