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:
parent
2bf6cd9241
commit
30f069a5db
21 changed files with 710 additions and 355 deletions
|
@ -75,7 +75,8 @@
|
||||||
"login_form_save_login": "Stay logged in",
|
"login_form_save_login": "Stay logged in",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
||||||
"profile_drawer_sign_out": "Sign Out",
|
"profile_drawer_sign_out": "Sign out",
|
||||||
|
"profile_drawer_settings": "Settings",
|
||||||
"search_bar_hint": "Search your photos",
|
"search_bar_hint": "Search your photos",
|
||||||
"search_page_no_objects": "No Objects Info Available",
|
"search_page_no_objects": "No Objects Info Available",
|
||||||
"search_page_no_places": "No Places Info Available",
|
"search_page_no_places": "No Places Info Available",
|
||||||
|
|
|
@ -16,3 +16,6 @@ const String backupInfoKey = "immichBackupAlbumInfoKey"; // Key 1
|
||||||
// Github Release Info
|
// Github Release Info
|
||||||
const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box
|
const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box
|
||||||
const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1
|
const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1
|
||||||
|
|
||||||
|
// User Setting Info
|
||||||
|
const String userSettingInfoBox = "immichUserSettingInfoBox";
|
||||||
|
|
|
@ -33,6 +33,7 @@ void main() async {
|
||||||
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
|
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
|
||||||
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
await Hive.openBox(hiveGithubReleaseInfoBox);
|
await Hive.openBox(hiveGithubReleaseInfoBox);
|
||||||
|
await Hive.openBox(userSettingInfoBox);
|
||||||
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(
|
const SystemUiOverlayStyle(
|
||||||
|
|
|
@ -56,11 +56,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _fireStartLoadingEvent() {
|
void _fireStartLoadingEvent() {
|
||||||
if (widget.onLoadingStart != null) widget.onLoadingStart!();
|
widget.onLoadingStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _fireFinishedLoadingEvent() {
|
void _fireFinishedLoadingEvent() {
|
||||||
if (widget.onLoadingCompleted != null) widget.onLoadingCompleted!();
|
widget.onLoadingCompleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
CachedNetworkImageProvider _authorizedImageProvider(String url) {
|
CachedNetworkImageProvider _authorizedImageProvider(String url) {
|
||||||
|
@ -141,26 +141,26 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemotePhotoView extends StatefulWidget {
|
class RemotePhotoView extends StatefulWidget {
|
||||||
const RemotePhotoView(
|
const RemotePhotoView({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.thumbnailUrl,
|
required this.thumbnailUrl,
|
||||||
required this.imageUrl,
|
required this.imageUrl,
|
||||||
required this.authToken,
|
required this.authToken,
|
||||||
required this.isZoomedFunction,
|
required this.isZoomedFunction,
|
||||||
required this.isZoomedListener,
|
required this.isZoomedListener,
|
||||||
required this.onSwipeDown,
|
required this.onSwipeDown,
|
||||||
required this.onSwipeUp,
|
required this.onSwipeUp,
|
||||||
this.previewUrl,
|
this.previewUrl,
|
||||||
this.onLoadingCompleted,
|
required this.onLoadingCompleted,
|
||||||
this.onLoadingStart})
|
required this.onLoadingStart,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String thumbnailUrl;
|
final String thumbnailUrl;
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
final String authToken;
|
final String authToken;
|
||||||
final String? previewUrl;
|
final String? previewUrl;
|
||||||
final Function? onLoadingCompleted;
|
final Function onLoadingCompleted;
|
||||||
final Function? onLoadingStart;
|
final Function onLoadingStart;
|
||||||
|
|
||||||
final void Function() onSwipeDown;
|
final void Function() onSwipeDown;
|
||||||
final void Function() onSwipeUp;
|
final void Function() onSwipeUp;
|
||||||
|
|
|
@ -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/image_viewer_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/video_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/modules/home/services/asset.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/app_settings.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
|
@ -18,8 +19,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
late List<AssetResponseDto> assetList;
|
late List<AssetResponseDto> assetList;
|
||||||
final AssetResponseDto asset;
|
final AssetResponseDto asset;
|
||||||
|
|
||||||
static const _threeStageLoading = false;
|
|
||||||
|
|
||||||
GalleryViewerPage({
|
GalleryViewerPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.assetList,
|
required this.assetList,
|
||||||
|
@ -27,21 +26,35 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
AssetResponseDto? assetDetail;
|
AssetResponseDto? assetDetail;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final Box<dynamic> box = Hive.box(userInfoBox);
|
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);
|
int indexOfAsset = assetList.indexOf(asset);
|
||||||
final loading = useState(false);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState(int index) {
|
|
||||||
indexOfAsset = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
PageController controller =
|
PageController controller =
|
||||||
PageController(initialPage: assetList.indexOf(asset));
|
PageController(initialPage: assetList.indexOf(asset));
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
threeStageLoading.value = appSettingService
|
||||||
|
.getSetting<bool>(AppSettingsEnum.threeStageLoading);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState(int index) {
|
||||||
|
indexOfAsset = index;
|
||||||
|
}
|
||||||
|
|
||||||
getAssetExif() async {
|
getAssetExif() async {
|
||||||
assetDetail = await ref
|
assetDetail = await ref
|
||||||
.watch(assetServiceProvider)
|
.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
|
//make isZoomed listener call instead
|
||||||
void isZoomedMethod() {
|
void isZoomedMethod() {
|
||||||
if (isZoomedListener.value) {
|
if (isZoomedListener.value) {
|
||||||
|
@ -84,7 +94,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
ref
|
ref
|
||||||
.watch(imageViewerStateProvider.notifier)
|
.watch(imageViewerStateProvider.notifier)
|
||||||
.downloadAsset(assetList[indexOfAsset], context);
|
.downloadAsset(assetList[indexOfAsset], context);
|
||||||
}, onSharePressed: () {
|
},
|
||||||
|
onSharePressed: () {
|
||||||
ref
|
ref
|
||||||
.watch(imageViewerStateProvider.notifier)
|
.watch(imageViewerStateProvider.notifier)
|
||||||
.shareAsset(assetList[indexOfAsset], context);
|
.shareAsset(assetList[indexOfAsset], context);
|
||||||
|
@ -101,17 +112,19 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
initState(index);
|
initState(index);
|
||||||
|
|
||||||
getAssetExif();
|
getAssetExif();
|
||||||
|
|
||||||
if (assetList[index].type == AssetTypeEnum.IMAGE) {
|
if (assetList[index].type == AssetTypeEnum.IMAGE) {
|
||||||
return ImageViewerPage(
|
return ImageViewerPage(
|
||||||
authToken: 'Bearer ${box.get(accessTokenKey)}',
|
authToken: 'Bearer ${box.get(accessTokenKey)}',
|
||||||
isZoomedFunction: isZoomedMethod,
|
isZoomedFunction: isZoomedMethod,
|
||||||
isZoomedListener: isZoomedListener,
|
isZoomedListener: isZoomedListener,
|
||||||
onLoadingCompleted: () => loading.value = false,
|
onLoadingCompleted: () => {},
|
||||||
onLoadingStart: () => loading.value = _threeStageLoading,
|
onLoadingStart: () => {},
|
||||||
asset: assetList[index],
|
asset: assetList[index],
|
||||||
heroTag: assetList[index].id,
|
heroTag: assetList[index].id,
|
||||||
threeStageLoading: _threeStageLoading
|
threeStageLoading: threeStageLoading.value,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return SwipeDetector(
|
return SwipeDetector(
|
||||||
|
|
|
@ -35,6 +35,7 @@ class ImageViewerPage extends HookConsumerWidget {
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
AssetResponseDto? assetDetail;
|
AssetResponseDto? assetDetail;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final downloadAssetStatus =
|
final downloadAssetStatus =
|
||||||
|
@ -71,18 +72,19 @@ class ImageViewerPage extends HookConsumerWidget {
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: heroTag,
|
tag: heroTag,
|
||||||
child: RemotePhotoView(
|
child: RemotePhotoView(
|
||||||
thumbnailUrl: getThumbnailUrl(asset),
|
thumbnailUrl: getThumbnailUrl(asset),
|
||||||
imageUrl: getImageUrl(asset),
|
imageUrl: getImageUrl(asset),
|
||||||
previewUrl: threeStageLoading
|
previewUrl: threeStageLoading
|
||||||
? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG)
|
? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG)
|
||||||
: null,
|
: null,
|
||||||
authToken: authToken,
|
authToken: authToken,
|
||||||
isZoomedFunction: isZoomedFunction,
|
isZoomedFunction: isZoomedFunction,
|
||||||
isZoomedListener: isZoomedListener,
|
isZoomedListener: isZoomedListener,
|
||||||
onSwipeDown: () => AutoRouter.of(context).pop(),
|
onSwipeDown: () => AutoRouter.of(context).pop(),
|
||||||
onSwipeUp: () => showInfo(),
|
onSwipeUp: () => showInfo(),
|
||||||
onLoadingCompleted: onLoadingCompleted,
|
onLoadingCompleted: onLoadingCompleted,
|
||||||
onLoadingStart: onLoadingStart),
|
onLoadingStart: onLoadingStart,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
||||||
|
|
|
@ -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),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
118
mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart
Normal file
118
mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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/image_grid.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.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/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/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
|
|
0
mobile/lib/modules/settings/models/store_model_here.txt
Normal file
0
mobile/lib/modules/settings/models/store_model_here.txt
Normal 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(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
81
mobile/lib/modules/settings/views/settings_page.dart
Normal file
81
mobile/lib/modules/settings/views/settings_page.dart
Normal 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: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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_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/select_user_for_sharing_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/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/routing/auth_guard.dart';
|
||||||
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
||||||
|
@ -77,6 +78,7 @@ part 'router.gr.dart';
|
||||||
guards: [AuthGuard],
|
guards: [AuthGuard],
|
||||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
),
|
),
|
||||||
|
AutoRoute(page: SettingsPage, guards: [AuthGuard]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppRouter extends _$AppRouter {
|
class AppRouter extends _$AppRouter {
|
||||||
|
|
|
@ -137,6 +137,10 @@ class _$AppRouter extends RootStackRouter {
|
||||||
opaque: true,
|
opaque: true,
|
||||||
barrierDismissible: false);
|
barrierDismissible: false);
|
||||||
},
|
},
|
||||||
|
SettingsRoute.name: (routeData) {
|
||||||
|
return MaterialPageX<dynamic>(
|
||||||
|
routeData: routeData, child: const SettingsPage());
|
||||||
|
},
|
||||||
HomeRoute.name: (routeData) {
|
HomeRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData, child: const HomePage());
|
routeData: routeData, child: const HomePage());
|
||||||
|
@ -211,7 +215,9 @@ class _$AppRouter extends RootStackRouter {
|
||||||
RouteConfig(AlbumPreviewRoute.name,
|
RouteConfig(AlbumPreviewRoute.name,
|
||||||
path: '/album-preview-page', guards: [authGuard]),
|
path: '/album-preview-page', guards: [authGuard]),
|
||||||
RouteConfig(FailedBackupStatusRoute.name,
|
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';
|
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
|
/// generated route for
|
||||||
/// [HomePage]
|
/// [HomePage]
|
||||||
class HomeRoute extends PageRouteInfo<void> {
|
class HomeRoute extends PageRouteInfo<void> {
|
||||||
|
|
79
mobile/lib/shared/services/app_settings.service.dart
Normal file
79
mobile/lib/shared/services/app_settings.service.dart
Normal 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());
|
|
@ -53,21 +53,23 @@ class TabControllerPage extends ConsumerWidget {
|
||||||
items: [
|
items: [
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
label: 'tab_controller_nav_photos'.tr(),
|
label: 'tab_controller_nav_photos'.tr(),
|
||||||
icon: const Icon(Icons.photo),
|
icon: const Icon(Icons.photo_outlined),
|
||||||
|
activeIcon: const Icon(Icons.photo),
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
label: 'tab_controller_nav_search'.tr(),
|
label: 'tab_controller_nav_search'.tr(),
|
||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.search_rounded),
|
||||||
|
activeIcon: const Icon(Icons.search),
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
label: 'tab_controller_nav_sharing'.tr(),
|
label: 'tab_controller_nav_sharing'.tr(),
|
||||||
icon: const Icon(Icons.group_outlined),
|
icon: const Icon(Icons.group_outlined),
|
||||||
|
activeIcon: const Icon(Icons.group),
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
label: 'tab_controller_nav_library'.tr(),
|
label: 'tab_controller_nav_library'.tr(),
|
||||||
icon: const Icon(
|
icon: const Icon(Icons.photo_album_outlined),
|
||||||
Icons.photo_album_outlined,
|
activeIcon: const Icon(Icons.photo_album_rounded),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue