From 30f069a5db7acf5ec46b4314b8735424ff8ed3ef Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 13 Aug 2022 15:51:09 -0500 Subject: [PATCH] Add settings screen on mobile (#463) * Refactor profile drawer to sub component * Added setting page, routing with some options * Added setting service * Implement three stage settings * get app setting for three stage loading --- mobile/assets/i18n/en-US.json | 3 +- mobile/lib/constants/hive_box.dart | 3 + mobile/lib/main.dart | 1 + .../asset_viewer/ui/remote_photo_view.dart | 34 +- .../asset_viewer/views/gallery_viewer.dart | 43 ++- .../asset_viewer/views/image_viewer_page.dart | 26 +- .../lib/modules/home/ui/profile_drawer.dart | 303 ------------------ .../ui/profile_drawer/profile_drawer.dart | 93 ++++++ .../profile_drawer/profile_drawer_header.dart | 166 ++++++++++ .../ui/profile_drawer/server_info_box.dart | 118 +++++++ mobile/lib/modules/home/views/home_page.dart | 2 +- .../settings/models/store_model_here.txt | 0 .../providers/store_providers_here.txt | 0 .../settings/services/store_services_here.txt | 0 .../image_viewer_quality_setting.dart | 29 ++ .../three_stage_loading.dart | 54 ++++ .../modules/settings/views/settings_page.dart | 81 +++++ mobile/lib/routing/router.dart | 2 + mobile/lib/routing/router.gr.dart | 16 +- .../shared/services/app_settings.service.dart | 79 +++++ .../lib/shared/views/tab_controller_page.dart | 12 +- 21 files changed, 710 insertions(+), 355 deletions(-) delete mode 100644 mobile/lib/modules/home/ui/profile_drawer.dart create mode 100644 mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart create mode 100644 mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart create mode 100644 mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart create mode 100644 mobile/lib/modules/settings/models/store_model_here.txt create mode 100644 mobile/lib/modules/settings/providers/store_providers_here.txt create mode 100644 mobile/lib/modules/settings/services/store_services_here.txt create mode 100644 mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart create mode 100644 mobile/lib/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart create mode 100644 mobile/lib/modules/settings/views/settings_page.dart create mode 100644 mobile/lib/shared/services/app_settings.service.dart diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 5e121daf6..14340dfda 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -75,7 +75,8 @@ "login_form_save_login": "Stay logged in", "monthly_title_text_date_format": "MMMM y", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", - "profile_drawer_sign_out": "Sign Out", + "profile_drawer_sign_out": "Sign out", + "profile_drawer_settings": "Settings", "search_bar_hint": "Search your photos", "search_page_no_objects": "No Objects Info Available", "search_page_no_places": "No Places Info Available", diff --git a/mobile/lib/constants/hive_box.dart b/mobile/lib/constants/hive_box.dart index 336e8fc4c..a82c7c9b9 100644 --- a/mobile/lib/constants/hive_box.dart +++ b/mobile/lib/constants/hive_box.dart @@ -16,3 +16,6 @@ const String backupInfoKey = "immichBackupAlbumInfoKey"; // Key 1 // Github Release Info const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1 + +// User Setting Info +const String userSettingInfoBox = "immichUserSettingInfoBox"; diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index d254ae917..1a3ba8b19 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -33,6 +33,7 @@ void main() async { await Hive.openBox(hiveLoginInfoBox); await Hive.openBox(hiveBackupInfoBox); await Hive.openBox(hiveGithubReleaseInfoBox); + await Hive.openBox(userSettingInfoBox); SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( diff --git a/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart b/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart index 862de5b50..3943c8a2c 100644 --- a/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart +++ b/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart @@ -56,11 +56,11 @@ class _RemotePhotoViewState extends State { } void _fireStartLoadingEvent() { - if (widget.onLoadingStart != null) widget.onLoadingStart!(); + widget.onLoadingStart(); } void _fireFinishedLoadingEvent() { - if (widget.onLoadingCompleted != null) widget.onLoadingCompleted!(); + widget.onLoadingCompleted(); } CachedNetworkImageProvider _authorizedImageProvider(String url) { @@ -141,26 +141,26 @@ class _RemotePhotoViewState extends State { } class RemotePhotoView extends StatefulWidget { - const RemotePhotoView( - {Key? key, - required this.thumbnailUrl, - required this.imageUrl, - required this.authToken, - required this.isZoomedFunction, - required this.isZoomedListener, - required this.onSwipeDown, - required this.onSwipeUp, - this.previewUrl, - this.onLoadingCompleted, - this.onLoadingStart}) - : super(key: key); + const RemotePhotoView({ + Key? key, + required this.thumbnailUrl, + required this.imageUrl, + required this.authToken, + required this.isZoomedFunction, + required this.isZoomedListener, + required this.onSwipeDown, + required this.onSwipeUp, + this.previewUrl, + required this.onLoadingCompleted, + required this.onLoadingStart, + }) : super(key: key); final String thumbnailUrl; final String imageUrl; final String authToken; final String? previewUrl; - final Function? onLoadingCompleted; - final Function? onLoadingStart; + final Function onLoadingCompleted; + final Function onLoadingStart; final void Function() onSwipeDown; final void Function() onSwipeUp; diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index 51535cf1a..602e24ee2 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -11,6 +11,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; import 'package:immich_mobile/modules/home/services/asset.service.dart'; +import 'package:immich_mobile/shared/services/app_settings.service.dart'; import 'package:openapi/api.dart'; // ignore: must_be_immutable @@ -18,8 +19,6 @@ class GalleryViewerPage extends HookConsumerWidget { late List assetList; final AssetResponseDto asset; - static const _threeStageLoading = false; - GalleryViewerPage({ Key? key, required this.assetList, @@ -27,21 +26,35 @@ class GalleryViewerPage extends HookConsumerWidget { }) : super(key: key); AssetResponseDto? assetDetail; + @override Widget build(BuildContext context, WidgetRef ref) { final Box box = Hive.box(userInfoBox); + final appSettingService = ref.watch(appSettingsServiceProvider); + final threeStageLoading = useState(false); + final loading = useState(false); + final isZoomed = useState(false); + ValueNotifier isZoomedListener = ValueNotifier(false); int indexOfAsset = assetList.indexOf(asset); - final loading = useState(false); - - @override - void initState(int index) { - indexOfAsset = index; - } PageController controller = PageController(initialPage: assetList.indexOf(asset)); + useEffect( + () { + threeStageLoading.value = appSettingService + .getSetting(AppSettingsEnum.threeStageLoading); + return null; + }, + [], + ); + + @override + initState(int index) { + indexOfAsset = index; + } + getAssetExif() async { assetDetail = await ref .watch(assetServiceProvider) @@ -60,9 +73,6 @@ class GalleryViewerPage extends HookConsumerWidget { ); } - final isZoomed = useState(false); - ValueNotifier isZoomedListener = ValueNotifier(false); - //make isZoomed listener call instead void isZoomedMethod() { if (isZoomedListener.value) { @@ -84,7 +94,8 @@ class GalleryViewerPage extends HookConsumerWidget { ref .watch(imageViewerStateProvider.notifier) .downloadAsset(assetList[indexOfAsset], context); - }, onSharePressed: () { + }, + onSharePressed: () { ref .watch(imageViewerStateProvider.notifier) .shareAsset(assetList[indexOfAsset], context); @@ -101,17 +112,19 @@ class GalleryViewerPage extends HookConsumerWidget { scrollDirection: Axis.horizontal, itemBuilder: (context, index) { initState(index); + getAssetExif(); + if (assetList[index].type == AssetTypeEnum.IMAGE) { return ImageViewerPage( authToken: 'Bearer ${box.get(accessTokenKey)}', isZoomedFunction: isZoomedMethod, isZoomedListener: isZoomedListener, - onLoadingCompleted: () => loading.value = false, - onLoadingStart: () => loading.value = _threeStageLoading, + onLoadingCompleted: () => {}, + onLoadingStart: () => {}, asset: assetList[index], heroTag: assetList[index].id, - threeStageLoading: _threeStageLoading + threeStageLoading: threeStageLoading.value, ); } else { return SwipeDetector( diff --git a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart index a4817ee9d..7f1ab2168 100644 --- a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart @@ -35,6 +35,7 @@ class ImageViewerPage extends HookConsumerWidget { }) : super(key: key); AssetResponseDto? assetDetail; + @override Widget build(BuildContext context, WidgetRef ref) { final downloadAssetStatus = @@ -71,18 +72,19 @@ class ImageViewerPage extends HookConsumerWidget { child: Hero( tag: heroTag, child: RemotePhotoView( - thumbnailUrl: getThumbnailUrl(asset), - imageUrl: getImageUrl(asset), - previewUrl: threeStageLoading - ? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG) - : null, - authToken: authToken, - isZoomedFunction: isZoomedFunction, - isZoomedListener: isZoomedListener, - onSwipeDown: () => AutoRouter.of(context).pop(), - onSwipeUp: () => showInfo(), - onLoadingCompleted: onLoadingCompleted, - onLoadingStart: onLoadingStart), + thumbnailUrl: getThumbnailUrl(asset), + imageUrl: getImageUrl(asset), + previewUrl: threeStageLoading + ? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG) + : null, + authToken: authToken, + isZoomedFunction: isZoomedFunction, + isZoomedListener: isZoomedListener, + onSwipeDown: () => AutoRouter.of(context).pop(), + onSwipeUp: () => showInfo(), + onLoadingCompleted: onLoadingCompleted, + onLoadingStart: onLoadingStart, + ), ), ), if (downloadAssetStatus == DownloadAssetStatus.loading) diff --git a/mobile/lib/modules/home/ui/profile_drawer.dart b/mobile/lib/modules/home/ui/profile_drawer.dart deleted file mode 100644 index 6154fdc31..000000000 --- a/mobile/lib/modules/home/ui/profile_drawer.dart +++ /dev/null @@ -1,303 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:immich_mobile/constants/hive_box.dart'; -import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/shared/providers/asset.provider.dart'; -import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; -import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; -import 'package:immich_mobile/shared/models/server_info_state.model.dart'; -import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; -import 'package:immich_mobile/shared/providers/server_info.provider.dart'; -import 'package:immich_mobile/shared/providers/websocket.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'dart:math'; - -class ProfileDrawer extends HookConsumerWidget { - const ProfileDrawer({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - String endpoint = Hive.box(userInfoBox).get(serverEndpointKey); - AuthenticationState authState = ref.watch(authenticationProvider); - ServerInfoState serverInfoState = ref.watch(serverInfoProvider); - final uploadProfileImageStatus = - ref.watch(uploadProfileImageProvider).status; - final appInfo = useState({}); - var dummmy = Random().nextInt(1024); - - _getPackageInfo() async { - PackageInfo packageInfo = await PackageInfo.fromPlatform(); - - appInfo.value = { - "version": packageInfo.version, - "buildNumber": packageInfo.buildNumber, - }; - } - - _buildUserProfileImage() { - if (authState.profileImagePath.isEmpty) { - return const CircleAvatar( - radius: 35, - backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), - backgroundColor: Colors.transparent, - ); - } - - if (uploadProfileImageStatus == UploadProfileStatus.idle) { - if (authState.profileImagePath.isNotEmpty) { - return CircleAvatar( - radius: 35, - backgroundImage: NetworkImage( - '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', - ), - backgroundColor: Colors.transparent, - ); - } else { - return const CircleAvatar( - radius: 35, - backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), - backgroundColor: Colors.transparent, - ); - } - } - - if (uploadProfileImageStatus == UploadProfileStatus.success) { - return CircleAvatar( - radius: 35, - backgroundImage: NetworkImage( - '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', - ), - backgroundColor: Colors.transparent, - ); - } - - if (uploadProfileImageStatus == UploadProfileStatus.failure) { - return const CircleAvatar( - radius: 35, - backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), - backgroundColor: Colors.transparent, - ); - } - - if (uploadProfileImageStatus == UploadProfileStatus.loading) { - return const ImmichLoadingIndicator(); - } - - return const SizedBox(); - } - - _pickUserProfileImage() async { - final XFile? image = await ImagePicker().pickImage( - source: ImageSource.gallery, - maxHeight: 1024, - maxWidth: 1024, - ); - - if (image != null) { - var success = - await ref.watch(uploadProfileImageProvider.notifier).upload(image); - - if (success) { - ref.watch(authenticationProvider.notifier).updateUserProfileImagePath( - ref.read(uploadProfileImageProvider).profileImagePath, - ); - } - } - } - - useEffect( - () { - _getPackageInfo(); - _buildUserProfileImage(); - return null; - }, - [], - ); - return Drawer( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ListView( - shrinkWrap: true, - padding: EdgeInsets.zero, - children: [ - DrawerHeader( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [ - Color.fromARGB(255, 216, 219, 238), - Color.fromARGB(255, 226, 230, 231) - ], - begin: Alignment.centerRight, - end: Alignment.centerLeft, - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - clipBehavior: Clip.none, - children: [ - _buildUserProfileImage(), - Positioned( - bottom: 0, - right: -5, - child: GestureDetector( - onTap: _pickUserProfileImage, - child: Material( - color: Colors.grey[50], - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50.0), - ), - child: Padding( - padding: const EdgeInsets.all(5.0), - child: Icon( - Icons.edit, - color: Theme.of(context).primaryColor, - size: 14, - ), - ), - ), - ), - ), - ], - ), - Text( - "${authState.firstName} ${authState.lastName}", - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - Text( - authState.userEmail, - style: TextStyle(color: Colors.grey[800], fontSize: 12), - ) - ], - ), - ), - ListTile( - tileColor: Colors.grey[100], - leading: const Icon( - Icons.logout_rounded, - color: Colors.black54, - ), - title: const Text( - "profile_drawer_sign_out", - style: TextStyle( - color: Colors.black54, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ).tr(), - onTap: () async { - bool res = - await ref.watch(authenticationProvider.notifier).logout(); - - if (res) { - ref.watch(backupProvider.notifier).cancelBackup(); - ref.watch(assetProvider.notifier).clearAllAsset(); - ref.watch(websocketProvider.notifier).disconnect(); - // AutoRouter.of(context).popUntilRoot(); - AutoRouter.of(context).replace(const LoginRoute()); - } - }, - ) - ], - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Card( - elevation: 0, - color: Colors.grey[100], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), // if you need this - side: const BorderSide( - color: Color.fromARGB(101, 201, 201, 201), - width: 1, - ), - ), - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - serverInfoState.isVersionMismatch - ? serverInfoState.versionMismatchErrorMessage - : "profile_drawer_client_server_up_to_date".tr(), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 11, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w600, - ), - ), - ), - const Divider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "App Version", - style: TextStyle( - fontSize: 11, - color: Colors.grey[500], - fontWeight: FontWeight.bold, - ), - ), - Text( - "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}", - style: TextStyle( - fontSize: 11, - color: Colors.grey[500], - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Divider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Server Version", - style: TextStyle( - fontSize: 11, - color: Colors.grey[500], - fontWeight: FontWeight.bold, - ), - ), - Text( - "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}", - style: TextStyle( - fontSize: 11, - color: Colors.grey[500], - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ], - ), - ), - ), - ) - ], - ), - ); - } -} diff --git a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart new file mode 100644 index 000000000..136f29b19 --- /dev/null +++ b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart @@ -0,0 +1,93 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer_header.dart'; +import 'package:immich_mobile/modules/home/ui/profile_drawer/server_info_box.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/providers/asset.provider.dart'; +import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; +import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; +import 'package:immich_mobile/shared/providers/websocket.provider.dart'; + +class ProfileDrawer extends HookConsumerWidget { + const ProfileDrawer({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + _buildSignoutButton() { + return ListTile( + horizontalTitleGap: 0, + leading: SizedBox( + height: double.infinity, + child: Icon( + Icons.logout_rounded, + color: Colors.grey[700], + size: 20, + ), + ), + title: Text( + "profile_drawer_sign_out", + style: TextStyle( + color: Colors.grey[700], + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ).tr(), + onTap: () async { + bool res = await ref.watch(authenticationProvider.notifier).logout(); + + if (res) { + ref.watch(backupProvider.notifier).cancelBackup(); + ref.watch(assetProvider.notifier).clearAllAsset(); + ref.watch(websocketProvider.notifier).disconnect(); + AutoRouter.of(context).replace(const LoginRoute()); + } + }, + ); + } + + _buildSettingButton() { + return ListTile( + horizontalTitleGap: 0, + leading: SizedBox( + height: double.infinity, + child: Icon( + Icons.settings_rounded, + color: Colors.grey[700], + size: 20, + ), + ), + title: Text( + "profile_drawer_settings", + style: TextStyle( + color: Colors.grey[700], + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ).tr(), + onTap: () { + AutoRouter.of(context).push(const SettingsRoute()); + }, + ); + } + + return Drawer( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ListView( + shrinkWrap: true, + padding: EdgeInsets.zero, + children: [ + const ProfileDrawerHeader(), + _buildSettingButton(), + _buildSignoutButton(), + ], + ), + const ServerInfoBox() + ], + ), + ); + } +} diff --git a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart new file mode 100644 index 000000000..50e88a7fa --- /dev/null +++ b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart @@ -0,0 +1,166 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:immich_mobile/constants/hive_box.dart'; +import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart'; +import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; +import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; +import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; + +class ProfileDrawerHeader extends HookConsumerWidget { + const ProfileDrawerHeader({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + String endpoint = Hive.box(userInfoBox).get(serverEndpointKey); + AuthenticationState authState = ref.watch(authenticationProvider); + final uploadProfileImageStatus = + ref.watch(uploadProfileImageProvider).status; + var dummmy = Random().nextInt(1024); + + _buildUserProfileImage() { + if (authState.profileImagePath.isEmpty) { + return const CircleAvatar( + radius: 35, + backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), + backgroundColor: Colors.transparent, + ); + } + + if (uploadProfileImageStatus == UploadProfileStatus.idle) { + if (authState.profileImagePath.isNotEmpty) { + return CircleAvatar( + radius: 35, + backgroundImage: NetworkImage( + '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', + ), + backgroundColor: Colors.transparent, + ); + } else { + return const CircleAvatar( + radius: 35, + backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), + backgroundColor: Colors.transparent, + ); + } + } + + if (uploadProfileImageStatus == UploadProfileStatus.success) { + return CircleAvatar( + radius: 35, + backgroundImage: NetworkImage( + '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', + ), + backgroundColor: Colors.transparent, + ); + } + + if (uploadProfileImageStatus == UploadProfileStatus.failure) { + return const CircleAvatar( + radius: 35, + backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), + backgroundColor: Colors.transparent, + ); + } + + if (uploadProfileImageStatus == UploadProfileStatus.loading) { + return const ImmichLoadingIndicator(); + } + + return const SizedBox(); + } + + _pickUserProfileImage() async { + final XFile? image = await ImagePicker().pickImage( + source: ImageSource.gallery, + maxHeight: 1024, + maxWidth: 1024, + ); + + if (image != null) { + var success = + await ref.watch(uploadProfileImageProvider.notifier).upload(image); + + if (success) { + ref.watch(authenticationProvider.notifier).updateUserProfileImagePath( + ref.read(uploadProfileImageProvider).profileImagePath, + ); + } + } + } + + useEffect( + () { + _buildUserProfileImage(); + return null; + }, + [], + ); + + return DrawerHeader( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + Color.fromARGB(255, 216, 219, 238), + Color.fromARGB(255, 242, 242, 242), + Colors.white, + ], + begin: Alignment.centerRight, + end: Alignment.centerLeft, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + _buildUserProfileImage(), + Positioned( + bottom: 0, + right: -5, + child: GestureDetector( + onTap: _pickUserProfileImage, + child: Material( + color: Colors.grey[50], + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50.0), + ), + child: Padding( + padding: const EdgeInsets.all(5.0), + child: Icon( + Icons.edit, + color: Theme.of(context).primaryColor, + size: 14, + ), + ), + ), + ), + ), + ], + ), + Text( + "${authState.firstName} ${authState.lastName}", + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), + Text( + authState.userEmail, + style: TextStyle(color: Colors.grey[800], fontSize: 12), + ) + ], + ), + ); + } +} diff --git a/mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart b/mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart new file mode 100644 index 000000000..158398343 --- /dev/null +++ b/mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/models/server_info_state.model.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:immich_mobile/shared/providers/server_info.provider.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class ServerInfoBox extends HookConsumerWidget { + const ServerInfoBox({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + ServerInfoState serverInfoState = ref.watch(serverInfoProvider); + + final appInfo = useState({}); + + _getPackageInfo() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + + appInfo.value = { + "version": packageInfo.version, + "buildNumber": packageInfo.buildNumber, + }; + } + + useEffect( + () { + _getPackageInfo(); + return null; + }, + [], + ); + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + elevation: 0, + color: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), // if you need this + side: const BorderSide( + color: Color.fromARGB(101, 201, 201, 201), + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + serverInfoState.isVersionMismatch + ? serverInfoState.versionMismatchErrorMessage + : "profile_drawer_client_server_up_to_date".tr(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 11, + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.w600, + ), + ), + ), + const Divider(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "App Version", + style: TextStyle( + fontSize: 11, + color: Colors.grey[500], + fontWeight: FontWeight.bold, + ), + ), + Text( + "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}", + style: TextStyle( + fontSize: 11, + color: Colors.grey[500], + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Divider(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Server Version", + style: TextStyle( + fontSize: 11, + color: Colors.grey[500], + fontWeight: FontWeight.bold, + ), + ), + Text( + "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}", + style: TextStyle( + fontSize: 11, + color: Colors.grey[500], + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index ff259a2fe..b28036ec5 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -9,7 +9,7 @@ import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; import 'package:immich_mobile/modules/home/ui/image_grid.dart'; import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart'; -import 'package:immich_mobile/modules/home/ui/profile_drawer.dart'; +import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; diff --git a/mobile/lib/modules/settings/models/store_model_here.txt b/mobile/lib/modules/settings/models/store_model_here.txt new file mode 100644 index 000000000..e69de29bb diff --git a/mobile/lib/modules/settings/providers/store_providers_here.txt b/mobile/lib/modules/settings/providers/store_providers_here.txt new file mode 100644 index 000000000..e69de29bb diff --git a/mobile/lib/modules/settings/services/store_services_here.txt b/mobile/lib/modules/settings/services/store_services_here.txt new file mode 100644 index 000000000..e69de29bb diff --git a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart new file mode 100644 index 000000000..9229ca11d --- /dev/null +++ b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart'; + +class ImageViewerQualitySetting extends StatelessWidget { + const ImageViewerQualitySetting({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return const ExpansionTile( + title: Text( + 'Image viewer quality', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text( + 'Adjust the quality of the detail image viewer', + style: TextStyle( + fontSize: 13, + ), + ), + children: [ + ThreeStageLoading(), + ], + ); + } +} diff --git a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart new file mode 100644 index 000000000..c1309448d --- /dev/null +++ b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/services/app_settings.service.dart'; + +class ThreeStageLoading extends HookConsumerWidget { + const ThreeStageLoading({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appSettingService = ref.watch(appSettingsServiceProvider); + + final isEnable = useState(false); + + useEffect( + () { + var isThreeStageLoadingEnable = + appSettingService.getSetting(AppSettingsEnum.threeStageLoading); + + isEnable.value = isThreeStageLoadingEnable; + return null; + }, + [], + ); + + void onSwitchChanged(bool switchValue) { + appSettingService.setSetting( + AppSettingsEnum.threeStageLoading, + switchValue, + ); + isEnable.value = switchValue; + } + + return SwitchListTile.adaptive( + title: const Text( + "Enable three stage loading", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + subtitle: const Text( + "The three-stage loading delivers the best quality image in exchange for a slower loading speed", + style: TextStyle( + fontSize: 12, + ), + ), + value: isEnable.value, + onChanged: onSwitchChanged, + ); + } +} diff --git a/mobile/lib/modules/settings/views/settings_page.dart b/mobile/lib/modules/settings/views/settings_page.dart new file mode 100644 index 000000000..387b3a953 --- /dev/null +++ b/mobile/lib/modules/settings/views/settings_page.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart'; + +class SettingsPage extends HookConsumerWidget { + const SettingsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + iconSize: 20, + splashRadius: 24, + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon(Icons.arrow_back_ios_new_rounded), + ), + automaticallyImplyLeading: false, + centerTitle: false, + title: const Text( + 'Settings', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + body: ListView( + children: [ + ...ListTile.divideTiles( + context: context, + tiles: [ + const ImageViewerQualitySetting(), + const SettingListTile( + title: 'Theme', + subtitle: 'Choose between light and dark theme', + ), + ], + ).toList(), + ], + ), + ); + } +} + +class SettingListTile extends StatelessWidget { + const SettingListTile({ + required this.title, + required this.subtitle, + Key? key, + }) : super(key: key); + + final String title; + final String subtitle; + + @override + Widget build(BuildContext context) { + return ListTile( + dense: true, + title: Text( + title, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text( + subtitle, + style: const TextStyle( + fontSize: 12, + ), + ), + trailing: const Icon( + Icons.keyboard_arrow_right_rounded, + size: 24, + ), + onTap: () {}, + ); + } +} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 4732e4d87..95edb9d9c 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -18,6 +18,7 @@ import 'package:immich_mobile/modules/album/views/create_album_page.dart'; import 'package:immich_mobile/modules/album/views/select_additional_user_for_sharing_page.dart'; import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart'; import 'package:immich_mobile/modules/album/views/sharing_page.dart'; +import 'package:immich_mobile/modules/settings/views/settings_page.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart'; import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; @@ -77,6 +78,7 @@ part 'router.gr.dart'; guards: [AuthGuard], transitionsBuilder: TransitionsBuilders.slideBottom, ), + AutoRoute(page: SettingsPage, guards: [AuthGuard]), ], ) class AppRouter extends _$AppRouter { diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index c047be73f..dd8faecd0 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -137,6 +137,10 @@ class _$AppRouter extends RootStackRouter { opaque: true, barrierDismissible: false); }, + SettingsRoute.name: (routeData) { + return MaterialPageX( + routeData: routeData, child: const SettingsPage()); + }, HomeRoute.name: (routeData) { return MaterialPageX( routeData: routeData, child: const HomePage()); @@ -211,7 +215,9 @@ class _$AppRouter extends RootStackRouter { RouteConfig(AlbumPreviewRoute.name, path: '/album-preview-page', guards: [authGuard]), RouteConfig(FailedBackupStatusRoute.name, - path: '/failed-backup-status-page', guards: [authGuard]) + path: '/failed-backup-status-page', guards: [authGuard]), + RouteConfig(SettingsRoute.name, + path: '/settings-page', guards: [authGuard]) ]; } @@ -546,6 +552,14 @@ class FailedBackupStatusRoute extends PageRouteInfo { static const String name = 'FailedBackupStatusRoute'; } +/// generated route for +/// [SettingsPage] +class SettingsRoute extends PageRouteInfo { + const SettingsRoute() : super(SettingsRoute.name, path: '/settings-page'); + + static const String name = 'SettingsRoute'; +} + /// generated route for /// [HomePage] class HomeRoute extends PageRouteInfo { diff --git a/mobile/lib/shared/services/app_settings.service.dart b/mobile/lib/shared/services/app_settings.service.dart new file mode 100644 index 000000000..dca81d00a --- /dev/null +++ b/mobile/lib/shared/services/app_settings.service.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/hive_box.dart'; + +enum AppSettingsEnum { + threeStageLoading, // true, false, + themeMode, // "light","dark" +} + +class AppSettingsService { + late final Box hiveBox; + + AppSettingsService() { + hiveBox = Hive.box(userSettingInfoBox); + } + + T getSetting(AppSettingsEnum settingType) { + var settingKey = _settingHiveBoxKeyLookup(settingType); + + if (!hiveBox.containsKey(settingKey)) { + T defaultSetting = _setDefaultSetting(settingType); + return defaultSetting; + } + + var result = hiveBox.get(settingKey); + + if (result is T) { + return result; + } else { + debugPrint("Incorrect setting type"); + throw TypeError(); + } + } + + setSetting(AppSettingsEnum settingType, T value) { + var settingKey = _settingHiveBoxKeyLookup(settingType); + + if (hiveBox.containsKey(settingKey)) { + var result = hiveBox.get(settingKey); + + if (result is! T) { + debugPrint("Incorrect setting type"); + throw TypeError(); + } + + hiveBox.put(settingKey, value); + } else { + hiveBox.put(settingKey, value); + } + } + + _setDefaultSetting(AppSettingsEnum settingType) { + var settingKey = _settingHiveBoxKeyLookup(settingType); + + // Default value of threeStageLoading is false + if (settingType == AppSettingsEnum.threeStageLoading) { + hiveBox.put(settingKey, false); + return false; + } + + // Default value of themeMode is "light" + if (settingType == AppSettingsEnum.themeMode) { + hiveBox.put(settingKey, "light"); + return "light"; + } + } + + String _settingHiveBoxKeyLookup(AppSettingsEnum settingType) { + switch (settingType) { + case AppSettingsEnum.threeStageLoading: + return 'threeStageLoading'; + case AppSettingsEnum.themeMode: + return 'themeMode'; + } + } +} + +final appSettingsServiceProvider = Provider((ref) => AppSettingsService()); diff --git a/mobile/lib/shared/views/tab_controller_page.dart b/mobile/lib/shared/views/tab_controller_page.dart index f15f3380e..a55aa5937 100644 --- a/mobile/lib/shared/views/tab_controller_page.dart +++ b/mobile/lib/shared/views/tab_controller_page.dart @@ -53,21 +53,23 @@ class TabControllerPage extends ConsumerWidget { items: [ BottomNavigationBarItem( label: 'tab_controller_nav_photos'.tr(), - icon: const Icon(Icons.photo), + icon: const Icon(Icons.photo_outlined), + activeIcon: const Icon(Icons.photo), ), BottomNavigationBarItem( label: 'tab_controller_nav_search'.tr(), - icon: const Icon(Icons.search), + icon: const Icon(Icons.search_rounded), + activeIcon: const Icon(Icons.search), ), BottomNavigationBarItem( label: 'tab_controller_nav_sharing'.tr(), icon: const Icon(Icons.group_outlined), + activeIcon: const Icon(Icons.group), ), BottomNavigationBarItem( label: 'tab_controller_nav_library'.tr(), - icon: const Icon( - Icons.photo_album_outlined, - ), + icon: const Icon(Icons.photo_album_outlined), + activeIcon: const Icon(Icons.photo_album_rounded), ) ], ),