mirror of https://github.com/immich-app/immich.git
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 loadingpull/470/head
parent
2bf6cd9241
commit
30f069a5db
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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());
|
||||||
Loading…
Reference in New Issue