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
This commit is contained in:
Alex 2022-08-13 15:51:09 -05:00 committed by GitHub
parent 2bf6cd9241
commit 30f069a5db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 710 additions and 355 deletions

View file

@ -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",

View file

@ -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";

View file

@ -33,6 +33,7 @@ void main() async {
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
await Hive.openBox(hiveGithubReleaseInfoBox);
await Hive.openBox(userSettingInfoBox);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(

View file

@ -56,11 +56,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
}
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<RemotePhotoView> {
}
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;

View file

@ -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<AssetResponseDto> 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<dynamic> box = Hive.box(userInfoBox);
final appSettingService = ref.watch(appSettingsServiceProvider);
final threeStageLoading = useState(false);
final loading = useState(false);
final isZoomed = useState<bool>(false);
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(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<bool>(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<bool>(false);
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(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(

View file

@ -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)

View file

@ -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,
),
),
],
),
],
),
),
),
)
],
),
);
}
}

View file

@ -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()
],
),
);
}
}

View file

@ -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),
)
],
),
);
}
}

View file

@ -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,
),
),
],
),
],
),
),
),
);
}
}

View file

@ -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';

View file

@ -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(),
],
);
}
}

View file

@ -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,
);
}
}

View file

@ -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: () {},
);
}
}

View file

@ -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 {

View file

@ -137,6 +137,10 @@ class _$AppRouter extends RootStackRouter {
opaque: true,
barrierDismissible: false);
},
SettingsRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData, child: const SettingsPage());
},
HomeRoute.name: (routeData) {
return MaterialPageX<dynamic>(
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<void> {
static const String name = 'FailedBackupStatusRoute';
}
/// generated route for
/// [SettingsPage]
class SettingsRoute extends PageRouteInfo<void> {
const SettingsRoute() : super(SettingsRoute.name, path: '/settings-page');
static const String name = 'SettingsRoute';
}
/// generated route for
/// [HomePage]
class HomeRoute extends PageRouteInfo<void> {

View file

@ -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<T>(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<T>(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());

View file

@ -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),
)
],
),