Compare commits

...

8 Commits

Author SHA1 Message Date
Alex Tran
1390d01763 Up version 2022-08-15 19:13:51 -05:00
Alex
86f780871c Fixed different lettercases in email create different user (#470)
* Fixed different lettercases in email create different user

* Fixed test
2022-08-15 19:11:08 -05:00
Alex
c1b22125fd Add mobile dark mode and user setting (#468)
* styling light and dark theme

* Icon topbar

* Fixed app bar title dark theme

* Fixed issue with getting thumbnail for things

* Refactor sharing page

* Refactor scroll thumb

* Refactor chip in auto  backup indiation button

* Refactor sharing page

* Added theme toggle

* Up version for testflight build

* Refactor backup controller page

* Refactor album selection page

* refactor album pages

* Refactor gradient color profile header

* Added theme switcher

* Register app theme correctly

* Added locale to the app

* Added translation key

* Styling for bottomsheet colors

* up server version

* Fixed font size

* Fixed overlapsed sliverappbar on photos screen
2022-08-15 18:53:30 -05:00
Alex
30f069a5db 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
2022-08-13 15:51:09 -05:00
bo0tzz
2bf6cd9241 Fix redirect to login page after password change (#461)
* Fix redirect to login page after password change

Copied from the similar fix in #414

* Fix typo in change-password form

* Remove misplaced text from user management page
2022-08-13 09:54:29 -05:00
Alex Tran
87d2a954a3 Fixed error handling with catch block 2022-08-12 22:29:24 -05:00
Alex
a388c5a642 Fixed webp upload on web (#460) 2022-08-12 21:52:30 -05:00
Alex Tran
4b34f017ca cosmetic change 2022-08-12 21:19:54 -05:00
65 changed files with 1589 additions and 982 deletions

View File

@@ -30,8 +30,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 32, "android.injected.version.code" => 33,
"android.injected.version.name" => "1.22.0", "android.injected.version.name" => "1.23.0",
} }
) )
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -0,0 +1,2 @@
* Added setting screen
* Implemented dark mode

View File

@@ -48,7 +48,7 @@
"control_bottom_app_bar_delete": "Delete", "control_bottom_app_bar_delete": "Delete",
"create_shared_album_page_share": "Share", "create_shared_album_page_share": "Share",
"create_shared_album_page_create": "Create", "create_shared_album_page_create": "Create",
"create_shared_album_page_share_add_assets": "ADD ASSETS", "create_shared_album_page_share_add_assets": "ADD PHOTOS",
"create_shared_album_page_share_select_photos": "Select Photos", "create_shared_album_page_share_select_photos": "Select Photos",
"daily_title_text_date": "E, MMM dd", "daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy", "daily_title_text_date_year": "E, MMM dd, yyyy",
@@ -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",
@@ -112,5 +113,14 @@
"library_page_new_album": "New album", "library_page_new_album": "New album",
"create_album_page_untitled": "Untitled", "create_album_page_untitled": "Untitled",
"share_dialog_preparing": "Preparing...", "share_dialog_preparing": "Preparing...",
"control_bottom_app_bar_share": "Share" "control_bottom_app_bar_share": "Share",
"setting_pages_app_bar_settings": "Settings",
"theme_setting_theme_title": "Theme",
"theme_setting_theme_subtitle": "Choose the app's theme setting",
"theme_setting_system_theme_switch": "Automatic (Follow system setting)",
"theme_setting_dark_mode_switch": "Dark mode",
"theme_setting_image_viewer_quality_title": "Image viewer quality",
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
"theme_setting_three_stage_loading_title": "Enable three-stage loading",
"theme_setting_three_stage_loading_subtitle": "The three-stage loading delivers the best quality image in exchange for a slower loading speed"
} }

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta" desc "iOS Beta"
lane :beta do lane :beta do
increment_version_number( increment_version_number(
version_number: "1.22.0" version_number: "1.23.0"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,

View File

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

View File

@@ -1,3 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const immichBackgroundColor = Color(0xFFf6f8fe); Color immichBackgroundColor = const Color(0xFFf6f8fe);
Color immichDarkBackgroundColor = const Color.fromARGB(255, 0, 0, 0);
Color immichDarkThemePrimaryColor = const Color.fromARGB(255, 173, 203, 250);

View File

@@ -7,7 +7,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
@@ -19,8 +18,10 @@ import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/release_info.provider.dart'; import 'package:immich_mobile/shared/providers/release_info.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.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/providers/websocket.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart'; import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'constants/hive_box.dart'; import 'constants/hive_box.dart';
void main() async { void main() async {
@@ -33,6 +34,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(
@@ -49,8 +51,11 @@ void main() async {
Locale('da', 'DK'), Locale('da', 'DK'),
Locale('de', 'DE'), Locale('de', 'DE'),
Locale('es', 'ES'), Locale('es', 'ES'),
Locale('fi', 'FI'),
Locale('fr', 'FR'), Locale('fr', 'FR'),
Locale('it', 'IT'), Locale('it', 'IT'),
Locale('ja', 'JP'),
Locale('pl', 'PL')
]; ];
if (kReleaseMode && Platform.isAndroid) { if (kReleaseMode && Platform.isAndroid) {
@@ -129,7 +134,6 @@ class ImmichAppState extends ConsumerState<ImmichApp>
@override @override
initState() { initState() {
super.initState(); super.initState();
initApp().then((_) => debugPrint("App Init Completed")); initApp().then((_) => debugPrint("App Init Completed"));
} }
@@ -154,23 +158,9 @@ class ImmichAppState extends ConsumerState<ImmichApp>
MaterialApp.router( MaterialApp.router(
title: 'Immich', title: 'Immich',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( themeMode: ref.watch(immichThemeProvider),
useMaterial3: true, darkTheme: immichDarkTheme,
brightness: Brightness.light, theme: immichLightTheme,
primarySwatch: Colors.indigo,
fontFamily: 'WorkSans',
snackBarTheme: const SnackBarThemeData(
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
),
scaffoldBackgroundColor: immichBackgroundColor,
appBarTheme: const AppBarTheme(
backgroundColor: immichBackgroundColor,
foregroundColor: Colors.indigo,
elevation: 1,
centerTitle: true,
systemOverlayStyle: SystemUiOverlayStyle.dark,
),
),
routeInformationParser: router.defaultRouteParser(), routeInformationParser: router.defaultRouteParser(),
routerDelegate: router.delegate( routerDelegate: router.delegate(
navigatorObservers: () => [TabNavigationObserver(ref: ref)], navigatorObservers: () => [TabNavigationObserver(ref: ref)],

View File

@@ -14,6 +14,8 @@ class AlbumActionOutlinedButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
return Padding( return Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: OutlinedButton.icon( child: OutlinedButton.icon(
@@ -22,19 +24,23 @@ class AlbumActionOutlinedButton extends StatelessWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
), ),
side: const BorderSide( side: BorderSide(
width: 1, width: 1,
color: Color.fromARGB(255, 215, 215, 215), color: isDarkTheme
? const Color.fromARGB(255, 63, 63, 63)
: const Color.fromARGB(255, 206, 206, 206),
), ),
), ),
icon: Icon(iconData, size: 15), icon: Icon(
iconData,
size: 15,
color: Theme.of(context).primaryColor,
),
label: Text( label: Text(
labelText, labelText,
style: const TextStyle( style: Theme.of(context).textTheme.labelSmall?.copyWith(
fontSize: 12, fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, ),
color: Colors.black87,
),
), ),
onPressed: onPressed, onPressed: onPressed,
), ),

View File

@@ -52,7 +52,6 @@ class AlbumThumbnailCard extends StatelessWidget {
album.albumName, album.albumName,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12,
), ),
), ),
), ),
@@ -65,14 +64,14 @@ class AlbumThumbnailCard extends StatelessWidget {
? 'album_thumbnail_card_item' ? 'album_thumbnail_card_item'
: 'album_thumbnail_card_items', : 'album_thumbnail_card_items',
style: const TextStyle( style: const TextStyle(
fontSize: 10, fontSize: 12,
), ),
).tr(args: ['${album.assetCount}']), ).tr(args: ['${album.assetCount}']),
if (album.shared) if (album.shared)
const Text( const Text(
'album_thumbnail_card_shared', 'album_thumbnail_card_shared',
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 12,
), ),
).tr() ).tr()
], ],

View File

@@ -19,6 +19,8 @@ class AlbumTitleTextField extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
return TextField( return TextField(
onChanged: (v) { onChanged: (v) {
if (v.isEmpty) { if (v.isEmpty) {
@@ -51,7 +53,10 @@ class AlbumTitleTextField extends ConsumerWidget {
albumTitleController.clear(); albumTitleController.clear();
isAlbumTitleEmpty.value = true; isAlbumTitleEmpty.value = true;
}, },
icon: const Icon(Icons.cancel_rounded), icon: Icon(
Icons.cancel_rounded,
color: Theme.of(context).primaryColor,
),
splashRadius: 10, splashRadius: 10,
) )
: null, : null,
@@ -65,7 +70,9 @@ class AlbumTitleTextField extends ConsumerWidget {
), ),
hintText: 'share_add_title'.tr(), hintText: 'share_add_title'.tr(),
focusColor: Colors.grey[300], focusColor: Colors.grey[300],
fillColor: Colors.grey[200], fillColor: isDarkTheme
? const Color.fromARGB(255, 32, 33, 35)
: Colors.grey[200],
filled: isAlbumTitleTextFieldFocus.value, filled: isAlbumTitleTextFieldFocus.value,
), ),
); );

View File

@@ -150,7 +150,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
void _buildBottomSheet() { void _buildBottomSheet() {
showModalBottomSheet( showModalBottomSheet(
backgroundColor: immichBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: false, isScrollControlled: false,
context: context, context: context,
builder: (context) { builder: (context) {

View File

@@ -18,6 +18,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final titleTextEditController = final titleTextEditController =
useTextEditingController(text: albumInfo.albumName); useTextEditingController(text: albumInfo.albumName);
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
void onFocusModeChange() { void onFocusModeChange() {
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) { if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
@@ -65,7 +66,10 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
onPressed: () { onPressed: () {
titleTextEditController.clear(); titleTextEditController.clear();
}, },
icon: const Icon(Icons.cancel_rounded), icon: Icon(
Icons.cancel_rounded,
color: Theme.of(context).primaryColor,
),
splashRadius: 10, splashRadius: 10,
) )
: null, : null,
@@ -78,7 +82,9 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
focusColor: Colors.grey[300], focusColor: Colors.grey[300],
fillColor: Colors.grey[200], fillColor: isDarkTheme
? const Color.fromARGB(255, 32, 33, 35)
: Colors.grey[200],
filled: titleFocusNode.hasFocus, filled: titleFocusNode.hasFocus,
hintText: 'share_add_title'.tr(), hintText: 'share_add_title'.tr(),
), ),

View File

@@ -35,13 +35,7 @@ class SharingSliverAppBar extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4.0),
child: TextButton.icon( child: ElevatedButton.icon(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Theme.of(context).primaryColor.withAlpha(20),
),
// foregroundColor: MaterialStateProperty.all(Colors.white),
),
onPressed: () { onPressed: () {
AutoRouter.of(context) AutoRouter.of(context)
.push(CreateAlbumRoute(isSharedAlbum: true)); .push(CreateAlbumRoute(isSharedAlbum: true));
@@ -52,8 +46,12 @@ class SharingSliverAppBar extends StatelessWidget {
), ),
label: const Text( label: const Text(
"sharing_silver_appbar_create_shared_album", "sharing_silver_appbar_create_shared_album",
style: maxLines: 1,
TextStyle(fontWeight: FontWeight.bold, fontSize: 12), style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 11,
// color: Theme.of(context).primaryColor,
),
).tr(), ).tr(),
), ),
), ),
@@ -61,13 +59,7 @@ class SharingSliverAppBar extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 4.0), padding: const EdgeInsets.only(left: 4.0),
child: TextButton.icon( child: ElevatedButton.icon(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Theme.of(context).primaryColor.withAlpha(20),
),
// foregroundColor: MaterialStateProperty.all(Colors.white),
),
onPressed: null, onPressed: null,
icon: const Icon( icon: const Icon(
Icons.swap_horizontal_circle_outlined, Icons.swap_horizontal_circle_outlined,
@@ -75,8 +67,11 @@ class SharingSliverAppBar extends StatelessWidget {
), ),
label: const Text( label: const Text(
"sharing_silver_appbar_share_partner", "sharing_silver_appbar_share_partner",
style: style: TextStyle(
TextStyle(fontWeight: FontWeight.bold, fontSize: 12), fontWeight: FontWeight.bold,
fontSize: 11,
),
maxLines: 1,
).tr(), ).tr(),
), ),
), ),

View File

@@ -3,7 +3,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
@@ -242,7 +241,7 @@ class AlbumViewerPage extends HookConsumerWidget {
titleFocusNode.unfocus(); titleFocusNode.unfocus();
}, },
child: DraggableScrollbar.semicircle( child: DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).hintColor,
controller: scrollController, controller: scrollController,
heightScrollThumb: 48.0, heightScrollThumb: 48.0,
child: CustomScrollView( child: CustomScrollView(
@@ -255,7 +254,7 @@ class AlbumViewerPage extends HookConsumerWidget {
minHeight: 50, minHeight: 50,
maxHeight: 50, maxHeight: 50,
child: Container( child: Container(
color: immichBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
child: _buildControlButton(albumInfo), child: _buildControlButton(albumInfo),
), ),
), ),

View File

@@ -43,7 +43,7 @@ class AssetSelectionPage extends HookConsumerWidget {
return Stack( return Stack(
children: [ children: [
DraggableScrollbar.semicircle( DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).hintColor,
controller: scrollController, controller: scrollController,
heightScrollThumb: 48.0, heightScrollThumb: 48.0,
child: CustomScrollView( child: CustomScrollView(

View File

@@ -27,6 +27,7 @@ class CreateAlbumPage extends HookConsumerWidget {
final isAlbumTitleEmpty = useState(true); final isAlbumTitleEmpty = useState(true);
final selectedAssets = final selectedAssets =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum; ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
_showSelectUserPage() { _showSelectUserPage() {
AutoRouter.of(context).push(const SelectUserForSharingRoute()); AutoRouter.of(context).push(const SelectUserForSharingRoute());
@@ -75,9 +76,12 @@ class CreateAlbumPage extends HookConsumerWidget {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 200, left: 18), padding: const EdgeInsets.only(top: 200, left: 18),
child: const Text( child: Text(
'create_shared_album_page_share_add_assets', 'create_shared_album_page_share_add_assets',
style: TextStyle(fontSize: 12), style: Theme.of(context).textTheme.headline2?.copyWith(
fontSize: 12,
fontWeight: FontWeight.normal,
),
).tr(), ).tr(),
), ),
); );
@@ -96,24 +100,28 @@ class CreateAlbumPage extends HookConsumerWidget {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: padding:
const EdgeInsets.symmetric(vertical: 22, horizontal: 16), const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
side: const BorderSide( side: BorderSide(
color: Color.fromARGB(255, 206, 206, 206), color: isDarkTheme
? const Color.fromARGB(255, 63, 63, 63)
: const Color.fromARGB(255, 206, 206, 206),
), ),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
), ),
), ),
onPressed: _onSelectPhotosButtonPressed, onPressed: _onSelectPhotosButtonPressed,
icon: const Icon(Icons.add_rounded), icon: Icon(
Icons.add_rounded,
color: Theme.of(context).primaryColor,
),
label: Padding( label: Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
child: Text( child: Text(
'create_shared_album_page_share_select_photos', 'create_shared_album_page_share_select_photos',
style: TextStyle( style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontSize: 16, fontSize: 16,
color: Colors.grey[700], fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, ),
),
).tr(), ).tr(),
), ),
), ),
@@ -190,6 +198,7 @@ class CreateAlbumPage extends HookConsumerWidget {
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,
centerTitle: false, centerTitle: false,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () {
ref.watch(assetSelectionProvider.notifier).removeAll(); ref.watch(assetSelectionProvider.notifier).removeAll();
@@ -197,9 +206,11 @@ class CreateAlbumPage extends HookConsumerWidget {
}, },
icon: const Icon(Icons.close_rounded), icon: const Icon(Icons.close_rounded),
), ),
title: const Text( title: Text(
'share_create_album', 'share_create_album',
style: TextStyle(color: Colors.black), style: Theme.of(context).textTheme.headline2?.copyWith(
color: Theme.of(context).primaryColor,
),
).tr(), ).tr(),
actions: [ actions: [
if (isSharedAlbum) if (isSharedAlbum)
@@ -209,8 +220,9 @@ class CreateAlbumPage extends HookConsumerWidget {
: null, : null,
child: Text( child: Text(
'create_shared_album_page_share'.tr(), 'create_shared_album_page_share'.tr(),
style: const TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
), ),
), ),
), ),
@@ -234,9 +246,9 @@ class CreateAlbumPage extends HookConsumerWidget {
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
elevation: 5, elevation: 5,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
// leading: Container(),
pinned: true, pinned: true,
floating: false, floating: false,
bottom: PreferredSize( bottom: PreferredSize(

View File

@@ -23,7 +23,7 @@ class LibraryPage extends HookConsumerWidget {
); );
Widget _buildAppBar() { Widget _buildAppBar() {
return SliverAppBar( return const SliverAppBar(
centerTitle: true, centerTitle: true,
floating: true, floating: true,
pinned: false, pinned: false,
@@ -35,7 +35,6 @@ class LibraryPage extends HookConsumerWidget {
fontFamily: 'SnowburstOne', fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 22, fontSize: 22,
color: Theme.of(context).primaryColor,
), ),
), ),
); );
@@ -72,7 +71,6 @@ class LibraryPage extends HookConsumerWidget {
child: const Text( child: const Text(
'library_page_new_album', 'library_page_new_album',
style: TextStyle( style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
).tr(), ).tr(),

View File

@@ -136,9 +136,9 @@ class SelectUserForSharingPage extends HookConsumerWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text( title: Text(
'share_invite', 'share_invite',
style: TextStyle(color: Colors.black), style: TextStyle(color: Theme.of(context).primaryColor),
).tr(), ).tr(),
elevation: 0, elevation: 0,
centerTitle: false, centerTitle: false,
@@ -150,11 +150,18 @@ class SelectUserForSharingPage extends HookConsumerWidget {
), ),
actions: [ actions: [
TextButton( TextButton(
style: TextButton.styleFrom(
primary: Theme.of(context).primaryColor,
),
onPressed: onPressed:
sharedUsersList.value.isEmpty ? null : _createSharedAlbum, sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
child: const Text( child: const Text(
"share_create_album", "share_create_album",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
// color: Theme.of(context).primaryColor,
),
).tr(), ).tr(),
) )
], ],

View File

@@ -61,11 +61,9 @@ class SharingPage extends HookConsumerWidget {
sharedAlbums[index].albumName, sharedAlbums[index].albumName,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16, fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, ),
color: Colors.grey.shade800,
),
), ),
onTap: () { onTap: () {
AutoRouter.of(context) AutoRouter.of(context)
@@ -87,7 +85,7 @@ class SharingPage extends HookConsumerWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // if you need this borderRadius: BorderRadius.circular(10), // if you need this
side: const BorderSide( side: const BorderSide(
color: Colors.black12, color: Colors.grey,
width: 1, width: 1,
), ),
), ),
@@ -97,30 +95,26 @@ class SharingPage extends HookConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( const Padding(
padding: const EdgeInsets.only(left: 5.0, bottom: 5), padding: EdgeInsets.only(left: 5.0, bottom: 5),
child: Icon( child: Icon(
Icons.offline_share_outlined, Icons.offline_share_outlined,
size: 50, size: 50,
color: Theme.of(context).primaryColor.withAlpha(200), // color: Theme.of(context).primaryColor,
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
'sharing_page_empty_list', 'sharing_page_empty_list',
style: TextStyle( style: Theme.of(context).textTheme.headline3,
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
).tr(), ).tr(),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
'sharing_page_description', 'sharing_page_description',
style: TextStyle(fontSize: 12, color: Colors.grey[700]), style: Theme.of(context).textTheme.bodyMedium,
).tr(), ).tr(),
), ),
], ],

View File

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

View File

@@ -11,6 +11,8 @@ 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/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/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 +20,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 +27,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 +74,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 +95,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 +113,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(

View File

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

View File

@@ -24,6 +24,7 @@ class AlbumInfoCard extends HookConsumerWidget {
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo); ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
final bool isExcluded = final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo); ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
ColorFilter selectedFilter = ColorFilter.mode( ColorFilter selectedFilter = ColorFilter.mode(
Theme.of(context).primaryColor.withAlpha(100), Theme.of(context).primaryColor.withAlpha(100),
@@ -39,11 +40,11 @@ class AlbumInfoCard extends HookConsumerWidget {
return Chip( return Chip(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
label: const Text( label: Text(
"album_info_card_backup_album_included", "album_info_card_backup_album_included",
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.white, color: isDarkTheme ? Colors.black : Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
).tr(), ).tr(),
@@ -53,11 +54,11 @@ class AlbumInfoCard extends HookConsumerWidget {
return Chip( return Chip(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
label: const Text( label: Text(
"album_info_card_backup_album_excluded", "album_info_card_backup_album_excluded",
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.white, color: isDarkTheme ? Colors.black : Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
).tr(), ).tr(),
@@ -141,8 +142,10 @@ class AlbumInfoCard extends HookConsumerWidget {
margin: const EdgeInsets.all(1), margin: const EdgeInsets.all(1),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), // if you need this borderRadius: BorderRadius.circular(12), // if you need this
side: const BorderSide( side: BorderSide(
color: Color(0xFFC9C9C9), color: isDarkTheme
? const Color.fromARGB(255, 37, 35, 35)
: const Color(0xFFC9C9C9),
width: 1, width: 1,
), ),
), ),
@@ -219,8 +222,9 @@ class AlbumInfoCard extends HookConsumerWidget {
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
AutoRouter.of(context) AutoRouter.of(context).push(
.push(AlbumPreviewRoute(album: albumInfo)); AlbumPreviewRoute(album: albumInfo),
);
}, },
icon: Icon( icon: Icon(
Icons.image_outlined, Icons.image_outlined,

View File

@@ -35,7 +35,7 @@ class BackupInfoCard extends StatelessWidget {
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Text( child: Text(
subtitle, subtitle,
style: const TextStyle(color: Color(0xFF808080), fontSize: 12), style: const TextStyle(fontSize: 12),
), ),
), ),
trailing: Column( trailing: Column(

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/ui/album_info_card.dart'; import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
@@ -16,6 +17,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
final availableAlbums = ref.watch(backupProvider).availableAlbums; final availableAlbums = ref.watch(backupProvider).availableAlbums;
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
useEffect( useEffect(
() { () {
@@ -81,14 +83,16 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
), ),
label: Text( label: Text(
album.name, album.name,
style: const TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.white, color: Theme.of(context).brightness == Brightness.dark
? Colors.black
: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
deleteIconColor: Colors.white, deleteIconColor: isDarkTheme ? Colors.black : Colors.white,
deleteIcon: const Icon( deleteIcon: const Icon(
Icons.cancel_rounded, Icons.cancel_rounded,
size: 15, size: 15,
@@ -119,14 +123,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
), ),
label: Text( label: Text(
album.name, album.name,
style: const TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.white, color: isDarkTheme ? Colors.black : immichBackgroundColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
backgroundColor: Colors.red[300], backgroundColor: Colors.red[300],
deleteIconColor: Colors.white, deleteIconColor:
isDarkTheme ? Colors.black : immichBackgroundColor,
deleteIcon: const Icon( deleteIcon: const Icon(
Icons.cancel_rounded, Icons.cancel_rounded,
size: 15, size: 15,
@@ -154,11 +159,16 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
children: [ children: [
Padding( Padding(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), vertical: 8.0,
horizontal: 16.0,
),
child: const Text( child: const Text(
"backup_album_selection_page_selection_info", "backup_album_selection_page_selection_info",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
).tr(), ).tr(),
), ),
// Selected Album Chips // Selected Album Chips
@@ -178,9 +188,11 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
child: Card( child: Card(
margin: const EdgeInsets.all(0), margin: const EdgeInsets.all(0),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), // if you need this borderRadius: BorderRadius.circular(5),
side: const BorderSide( side: BorderSide(
color: Color.fromARGB(255, 235, 235, 235), color: isDarkTheme
? const Color.fromARGB(255, 0, 0, 0)
: const Color.fromARGB(255, 235, 235, 235),
width: 1, width: 1,
), ),
), ),
@@ -190,12 +202,11 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
children: [ children: [
ListTile( ListTile(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
title: Text( title: const Text(
"backup_album_selection_page_total_assets", "backup_album_selection_page_total_assets",
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
color: Colors.grey[700],
), ),
).tr(), ).tr(),
trailing: Text( trailing: Text(
@@ -257,11 +268,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
content: SingleChildScrollView( content: SingleChildScrollView(
child: ListBody( child: ListBody(
children: [ children: [
Text( const Text(
'backup_album_selection_page_assets_scatter', 'backup_album_selection_page_assets_scatter',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.grey[700],
), ),
).tr(), ).tr(),
], ],

View File

@@ -82,7 +82,7 @@ class BackupControllerPage extends HookConsumerWidget {
); );
} }
ListTile _buildBackupController() { ListTile _buildAutoBackupController() {
var backUpOption = authenticationState.deviceInfo.isAutoBackup var backUpOption = authenticationState.deviceInfo.isAutoBackup
? "backup_controller_page_status_on".tr() ? "backup_controller_page_status_on".tr()
: "backup_controller_page_status_off".tr(); : "backup_controller_page_status_off".tr();
@@ -114,13 +114,7 @@ class BackupControllerPage extends HookConsumerWidget {
).tr(), ).tr(),
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: OutlinedButton( child: ElevatedButton(
style: OutlinedButton.styleFrom(
side: const BorderSide(
width: 1,
color: Color.fromARGB(255, 220, 220, 220),
),
),
onPressed: () { onPressed: () {
if (isAutoBackup) { if (isAutoBackup) {
ref ref
@@ -134,7 +128,10 @@ class BackupControllerPage extends HookConsumerWidget {
}, },
child: Text( child: Text(
backupBtnText, backupBtnText,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
), ),
), ),
) )
@@ -232,33 +229,24 @@ class BackupControllerPage extends HookConsumerWidget {
children: [ children: [
const Text( const Text(
"backup_controller_page_to_backup", "backup_controller_page_to_backup",
style: TextStyle(color: Color(0xFF808080), fontSize: 12), style: TextStyle(fontSize: 12),
).tr(), ).tr(),
_buildSelectedAlbumName(), _buildSelectedAlbumName(),
_buildExcludedAlbumName() _buildExcludedAlbumName()
], ],
), ),
), ),
trailing: OutlinedButton( trailing: ElevatedButton(
style: OutlinedButton.styleFrom(
enableFeedback: true,
side: const BorderSide(
width: 1,
color: Color.fromARGB(255, 220, 220, 220),
),
),
onPressed: () { onPressed: () {
AutoRouter.of(context).push(const BackupAlbumSelectionRoute()); AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
}, },
child: Padding( child: const Text(
padding: const EdgeInsets.symmetric( "backup_controller_page_select",
vertical: 16.0, style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
), ),
child: const Text( ).tr(),
"backup_controller_page_select",
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
),
), ),
), ),
); );
@@ -324,14 +312,14 @@ class BackupControllerPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Table( child: Table(
border: TableBorder.all( border: TableBorder.all(
color: Colors.black12, color: Theme.of(context).primaryColorLight,
width: 1, width: 1,
), ),
children: [ children: [
TableRow( TableRow(
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.grey[100], // color: Colors.grey[100],
), ),
children: [ children: [
TableCell( TableCell(
verticalAlignment: TableCellVerticalAlignment.middle, verticalAlignment: TableCellVerticalAlignment.middle,
@@ -355,9 +343,9 @@ class BackupControllerPage extends HookConsumerWidget {
], ],
), ),
TableRow( TableRow(
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.grey[200], // color: Colors.grey[200],
), ),
children: [ children: [
TableCell( TableCell(
verticalAlignment: TableCellVerticalAlignment.middle, verticalAlignment: TableCellVerticalAlignment.middle,
@@ -384,9 +372,9 @@ class BackupControllerPage extends HookConsumerWidget {
], ],
), ),
TableRow( TableRow(
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.grey[100], // color: Colors.grey[100],
), ),
children: [ children: [
TableCell( TableCell(
child: Padding( child: Padding(
@@ -463,7 +451,7 @@ class BackupControllerPage extends HookConsumerWidget {
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}", "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
), ),
const Divider(), const Divider(),
_buildBackupController(), _buildAutoBackupController(),
const Divider(), const Divider(),
_buildStorageInformation(), _buildStorageInformation(),
const Divider(), const Divider(),
@@ -479,7 +467,7 @@ class BackupControllerPage extends HookConsumerWidget {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
primary: Colors.red[300], primary: Colors.red[300],
onPrimary: Colors.grey[50], onPrimary: Colors.grey[50],
padding: const EdgeInsets.all(14), // padding: const EdgeInsets.all(14),
), ),
onPressed: () { onPressed: () {
ref.read(backupProvider.notifier).cancelBackup(); ref.read(backupProvider.notifier).cancelBackup();
@@ -493,11 +481,6 @@ class BackupControllerPage extends HookConsumerWidget {
).tr(), ).tr(),
) )
: ElevatedButton( : ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).primaryColor,
onPrimary: Colors.grey[50],
padding: const EdgeInsets.all(14),
),
onPressed: shouldBackup ? startBackup : null, onPressed: shouldBackup ? startBackup : null,
child: const Text( child: const Text(
"backup_controller_page_start_backup", "backup_controller_page_start_backup",

View File

@@ -1,11 +1,9 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart'; import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
import '../../../shared/providers/asset.provider.dart';
import '../providers/home_page_state.provider.dart';
class ControlBottomAppBar extends ConsumerWidget { class ControlBottomAppBar extends ConsumerWidget {
const ControlBottomAppBar({Key? key}) : super(key: key); const ControlBottomAppBar({Key? key}) : super(key: key);
@@ -19,10 +17,10 @@ class ControlBottomAppBar extends ConsumerWidget {
height: MediaQuery.of(context).size.height * 0.15, height: MediaQuery.of(context).size.height * 0.15,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(15), topLeft: Radius.circular(8),
topRight: Radius.circular(15), topRight: Radius.circular(8),
), ),
color: Colors.grey[300]?.withOpacity(0.98), color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.95),
), ),
child: Column( child: Column(
children: [ children: [

View File

@@ -86,7 +86,6 @@ class DailyTitleText extends ConsumerWidget {
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.black87,
), ),
), ),
const Spacer(), const Spacer(),

View File

@@ -14,32 +14,22 @@ class DisableMultiSelectButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Positioned( return Positioned(
top: 0, top: 10,
left: 0, left: 0,
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 16.0, top: 46), padding: const EdgeInsets.only(left: 16.0, top: 46),
child: Material( child: Padding(
elevation: 20, padding: const EdgeInsets.symmetric(horizontal: 4.0),
borderRadius: BorderRadius.circular(35), child: ElevatedButton.icon(
child: Container( onPressed: () {
decoration: BoxDecoration( onPressed();
borderRadius: BorderRadius.circular(35), },
color: Colors.grey[100], icon: const Icon(Icons.close_rounded),
), label: Text(
child: Padding( '$selectedItemCount',
padding: const EdgeInsets.symmetric(horizontal: 4.0), style: const TextStyle(
child: TextButton.icon( fontWeight: FontWeight.w600,
onPressed: () { fontSize: 18,
onPressed();
},
icon: const Icon(Icons.close_rounded),
label: Text(
'$selectedItemCount',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
), ),
), ),
), ),

View File

@@ -30,6 +30,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
floating: true, floating: true,
pinned: false, pinned: false,
snap: false, snap: false,
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)), borderRadius: BorderRadius.all(Radius.circular(5)),
), ),
@@ -57,7 +58,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
child: GestureDetector( child: GestureDetector(
onTap: () => Scaffold.of(context).openDrawer(), onTap: () => Scaffold.of(context).openDrawer(),
child: Material( child: Material(
color: Colors.grey[200], // color: Colors.grey[200],
elevation: 1, elevation: 1,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50.0), borderRadius: BorderRadius.circular(50.0),
@@ -77,13 +78,12 @@ class ImmichSliverAppBar extends ConsumerWidget {
); );
}, },
), ),
title: Text( title: const Text(
'IMMICH', 'IMMICH',
style: TextStyle( style: TextStyle(
fontFamily: 'SnowburstOne', fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 22, fontSize: 22,
color: Theme.of(context).primaryColor,
), ),
), ),
actions: [ actions: [
@@ -112,12 +112,13 @@ class ImmichSliverAppBar extends ConsumerWidget {
? const Icon(Icons.backup_rounded) ? const Icon(Icons.backup_rounded)
: Badge( : Badge(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
elevation: 2, elevation: 3,
position: BadgePosition.bottomEnd(bottom: -4, end: -4), position: BadgePosition.bottomEnd(bottom: -4, end: -4),
badgeColor: Colors.white, badgeColor: Colors.white,
badgeContent: const Icon( badgeContent: const Icon(
Icons.cloud_off_rounded, Icons.cloud_off_rounded,
size: 8, size: 8,
color: Colors.indigo,
), ),
child: const Icon(Icons.backup_rounded), child: const Icon(Icons.backup_rounded),
), ),

View File

@@ -22,7 +22,7 @@ class MonthlyTitleText extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: 26, fontSize: 26,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor, color: Theme.of(context).textTheme.headline1?.color,
), ),
), ),
), ),

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,91 @@
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: Theme.of(context).textTheme.labelMedium?.color,
size: 20,
),
),
title: Text(
"profile_drawer_sign_out",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(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: Theme.of(context).textTheme.labelMedium?.color,
size: 20,
),
),
title: Text(
"profile_drawer_settings",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(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,173 @@
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);
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
_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: BoxDecoration(
gradient: LinearGradient(
colors: isDarkMode
? [
const Color.fromARGB(255, 22, 25, 48),
const Color.fromARGB(255, 13, 13, 13),
const Color.fromARGB(255, 0, 0, 0),
]
: [
const Color.fromARGB(255, 216, 219, 238),
const 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[100],
elevation: 3,
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: Theme.of(context).textTheme.labelMedium,
)
],
),
);
}
}

View File

@@ -0,0 +1,124 @@
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: Theme.of(context).scaffoldBackgroundColor,
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(
color: Color.fromARGB(101, 201, 201, 201),
thickness: 1,
),
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(
color: Color.fromARGB(101, 201, 201, 201),
thickness: 1,
),
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/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';
@@ -117,9 +117,9 @@ class HomePage extends HookConsumerWidget {
], ],
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 50.0), padding: const EdgeInsets.only(top: 60.0, bottom: 30.0),
child: DraggableScrollbar.semicircle( child: DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).hintColor,
controller: scrollController, controller: scrollController,
heightScrollThumb: 48.0, heightScrollThumb: 48.0,
child: CustomScrollView( child: CustomScrollView(

View File

@@ -26,7 +26,7 @@ class ThumbnailWithInfo extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: SizedBox( child: SizedBox(
width: MediaQuery.of(context).size.width / 2, width: MediaQuery.of(context).size.width / 3,
child: Stack( child: Stack(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: [ children: [
@@ -58,7 +58,7 @@ class ThumbnailWithInfo extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16, fontSize: 14,
), ),
), ),
), ),

View File

@@ -29,6 +29,8 @@ class SearchPage extends HookConsumerWidget {
AsyncValue<List<CuratedObjectsResponseDto>> curatedObjects = AsyncValue<List<CuratedObjectsResponseDto>> curatedObjects =
ref.watch(getCuratedObjectProvider); ref.watch(getCuratedObjectProvider);
double imageSize = MediaQuery.of(context).size.width / 3;
useEffect( useEffect(
() { () {
searchFocusNode = FocusNode(); searchFocusNode = FocusNode();
@@ -46,15 +48,15 @@ class SearchPage extends HookConsumerWidget {
_buildPlaces() { _buildPlaces() {
return curatedLocation.when( return curatedLocation.when(
loading: () => const SizedBox( loading: () => SizedBox(
height: 200, height: imageSize,
child: Center(child: ImmichLoadingIndicator()), child: const Center(child: ImmichLoadingIndicator()),
), ),
error: (err, stack) => Text('Error: $err'), error: (err, stack) => Text('Error: $err'),
data: (curatedLocations) { data: (curatedLocations) {
return curatedLocations.isNotEmpty return curatedLocations.isNotEmpty
? SizedBox( ? SizedBox(
height: MediaQuery.of(context).size.width / 2, height: imageSize,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(left: 16), padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@@ -76,7 +78,7 @@ class SearchPage extends HookConsumerWidget {
), ),
) )
: SizedBox( : SizedBox(
height: MediaQuery.of(context).size.width / 2, height: imageSize,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(left: 16), padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@@ -105,7 +107,7 @@ class SearchPage extends HookConsumerWidget {
data: (objects) { data: (objects) {
return objects.isNotEmpty return objects.isNotEmpty
? SizedBox( ? SizedBox(
height: MediaQuery.of(context).size.width / 2, height: imageSize,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(left: 16), padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@@ -113,7 +115,7 @@ class SearchPage extends HookConsumerWidget {
itemBuilder: ((context, index) { itemBuilder: ((context, index) {
var curatedObjectInfo = objects[index]; var curatedObjectInfo = objects[index];
var thumbnailRequestUrl = var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/file?aid=${curatedObjectInfo.deviceAssetId}&did=${curatedObjectInfo.deviceId}&isThumb=true'; '${box.get(serverEndpointKey)}/asset/thumbnail/${curatedObjectInfo.id}';
return ThumbnailWithInfo( return ThumbnailWithInfo(
imageUrl: thumbnailRequestUrl, imageUrl: thumbnailRequestUrl,
@@ -131,7 +133,8 @@ class SearchPage extends HookConsumerWidget {
), ),
) )
: SizedBox( : SizedBox(
height: MediaQuery.of(context).size.width / 2, // height: imageSize,
width: imageSize,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(left: 16), padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@@ -163,12 +166,13 @@ class SearchPage extends HookConsumerWidget {
child: Stack( child: Stack(
children: [ children: [
ListView( ListView(
shrinkWrap: true,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: const Text( child: const Text(
"search_page_places", "search_page_places",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
).tr(), ).tr(),
), ),
_buildPlaces(), _buildPlaces(),
@@ -176,7 +180,7 @@ class SearchPage extends HookConsumerWidget {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: const Text( child: const Text(
"search_page_things", "search_page_things",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
).tr(), ).tr(),
), ),
_buildThings() _buildThings()

View File

@@ -172,7 +172,7 @@ class SearchResultPage extends HookConsumerWidget {
}); });
return DraggableScrollbar.semicircle( return DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).hintColor,
controller: scrollController, controller: scrollController,
heightScrollThumb: 48.0, heightScrollThumb: 48.0,
child: CustomScrollView( child: CustomScrollView(

View File

@@ -0,0 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
final appSettingsServiceProvider = Provider((ref) => AppSettingsService());

View File

@@ -0,0 +1,77 @@
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","system"
}
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, "system");
return "system";
}
}
String _settingHiveBoxKeyLookup(AppSettingsEnum settingType) {
switch (settingType) {
case AppSettingsEnum.threeStageLoading:
return 'threeStageLoading';
case AppSettingsEnum.themeMode:
return 'themeMode';
}
}
}

View File

@@ -0,0 +1,31 @@
import 'package:easy_localization/easy_localization.dart';
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 ExpansionTile(
textColor: Theme.of(context).primaryColor,
title: const Text(
'theme_setting_image_viewer_quality_title',
style: TextStyle(
fontWeight: FontWeight.bold,
),
).tr(),
subtitle: const Text(
'theme_setting_image_viewer_quality_subtitle',
style: TextStyle(
fontSize: 13,
),
).tr(),
children: const [
ThreeStageLoading(),
],
);
}
}

View File

@@ -0,0 +1,56 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/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(
"theme_setting_three_stage_loading_title",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
).tr(),
subtitle: const Text(
"theme_setting_three_stage_loading_subtitle",
style: TextStyle(
fontSize: 12,
),
).tr(),
value: isEnable.value,
onChanged: onSwitchChanged,
);
}
}

View File

@@ -0,0 +1,107 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/utils/immich_app_theme.dart';
class ThemeSetting extends HookConsumerWidget {
const ThemeSetting({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentTheme = useState<ThemeMode>(ThemeMode.system);
useEffect(
() {
currentTheme.value = ref.read(immichThemeProvider);
return null;
},
[],
);
return ExpansionTile(
textColor: Theme.of(context).primaryColor,
title: const Text(
'theme_setting_theme_title',
style: TextStyle(
fontWeight: FontWeight.bold,
),
).tr(),
subtitle: const Text(
'theme_setting_theme_subtitle',
style: TextStyle(
fontSize: 13,
),
).tr(),
children: [
SwitchListTile.adaptive(
activeColor: Theme.of(context).primaryColor,
title: const Text(
'theme_setting_system_theme_switch',
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
),
).tr(),
value: currentTheme.value == ThemeMode.system,
onChanged: (bool isSystem) {
var currentSystemBrightness =
MediaQuery.of(context).platformBrightness;
if (isSystem) {
currentTheme.value = ThemeMode.system;
ref.watch(immichThemeProvider.notifier).state = ThemeMode.system;
ref
.watch(appSettingsServiceProvider)
.setSetting(AppSettingsEnum.themeMode, "system");
} else {
if (currentSystemBrightness == Brightness.light) {
currentTheme.value = ThemeMode.light;
ref.watch(immichThemeProvider.notifier).state = ThemeMode.light;
ref
.watch(appSettingsServiceProvider)
.setSetting(AppSettingsEnum.themeMode, "light");
} else if (currentSystemBrightness == Brightness.dark) {
currentTheme.value = ThemeMode.dark;
ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark;
ref
.watch(appSettingsServiceProvider)
.setSetting(AppSettingsEnum.themeMode, "dark");
}
}
},
),
if (currentTheme.value != ThemeMode.system)
SwitchListTile.adaptive(
activeColor: Theme.of(context).primaryColor,
title: const Text(
'theme_setting_dark_mode_switch',
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
),
).tr(),
value: ref.watch(immichThemeProvider) == ThemeMode.dark,
onChanged: (bool isDark) {
if (isDark) {
ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark;
ref
.watch(appSettingsServiceProvider)
.setSetting(AppSettingsEnum.themeMode, "dark");
} else {
ref.watch(immichThemeProvider.notifier).state = ThemeMode.light;
ref
.watch(appSettingsServiceProvider)
.setSetting(AppSettingsEnum.themeMode, "light");
}
},
),
],
);
}
}

View File

@@ -0,0 +1,45 @@
import 'package:easy_localization/easy_localization.dart';
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';
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_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(
'setting_pages_app_bar_settings',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
).tr(),
),
body: ListView(
children: [
...ListTile.divideTiles(
context: context,
tiles: [
const ImageViewerQualitySetting(),
const ThemeSetting(),
],
).toList(),
],
),
);
}
}

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

View File

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

View File

@@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@@ -49,7 +48,6 @@ class SplashScreenPage extends HookConsumerWidget {
); );
return Scaffold( return Scaffold(
backgroundColor: immichBackgroundColor,
body: Center( body: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@@ -2,7 +2,6 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@@ -36,8 +35,6 @@ class TabControllerPage extends ConsumerWidget {
bottomNavigationBar: isMultiSelectEnable bottomNavigationBar: isMultiSelectEnable
? null ? null
: BottomNavigationBar( : BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: immichBackgroundColor,
selectedLabelStyle: const TextStyle( selectedLabelStyle: const TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -53,21 +50,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),
),
) )
], ],
), ),

View File

@@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
final immichThemeProvider = StateProvider<ThemeMode>((ref) {
var themeMode = ref
.watch(appSettingsServiceProvider)
.getSetting(AppSettingsEnum.themeMode);
debugPrint("Current themeMode $themeMode");
if (themeMode == "light") {
return ThemeMode.light;
} else if (themeMode == "dark") {
return ThemeMode.dark;
} else {
return ThemeMode.system;
}
});
ThemeData immichDarkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
primarySwatch: Colors.indigo,
primaryColor: immichDarkThemePrimaryColor,
scaffoldBackgroundColor: immichDarkBackgroundColor,
hintColor: Colors.grey[600],
fontFamily: 'WorkSans',
snackBarTheme: const SnackBarThemeData(
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
),
appBarTheme: AppBarTheme(
titleTextStyle: TextStyle(
fontFamily: 'WorkSans',
color: immichDarkThemePrimaryColor,
),
backgroundColor: const Color.fromARGB(255, 32, 33, 35),
foregroundColor: immichDarkThemePrimaryColor,
elevation: 1,
centerTitle: true,
systemOverlayStyle: SystemUiOverlayStyle.light,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: const Color.fromARGB(255, 35, 36, 37),
selectedItemColor: immichDarkThemePrimaryColor,
),
drawerTheme: DrawerThemeData(
backgroundColor: immichDarkBackgroundColor,
scrimColor: Colors.white.withOpacity(0.1),
),
textTheme: TextTheme(
headline1: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 255, 255, 255),
),
headline2: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 148, 151, 155),
),
headline3: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: immichDarkThemePrimaryColor,
),
),
cardColor: Colors.grey[900],
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
onPrimary: Colors.black87,
primary: immichDarkThemePrimaryColor,
),
),
);
ThemeData immichLightTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.light,
primarySwatch: Colors.indigo,
hintColor: Colors.indigo,
fontFamily: 'WorkSans',
scaffoldBackgroundColor: immichBackgroundColor,
snackBarTheme: const SnackBarThemeData(
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
),
appBarTheme: AppBarTheme(
titleTextStyle: const TextStyle(
fontFamily: 'WorkSans',
color: Colors.indigo,
),
backgroundColor: immichBackgroundColor,
foregroundColor: Colors.indigo,
elevation: 1,
centerTitle: true,
systemOverlayStyle: SystemUiOverlayStyle.dark,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: immichBackgroundColor,
selectedItemColor: Colors.indigo,
),
drawerTheme: DrawerThemeData(
backgroundColor: immichBackgroundColor,
),
textTheme: const TextTheme(
headline1: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
headline2: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
headline3: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
primary: Colors.indigo,
onPrimary: Colors.white,
),
),
);

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: "none" publish_to: "none"
version: 1.22.0+32 version: 1.23.0+33
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"

View File

@@ -25,7 +25,7 @@ export class AuthController {
@Post('/login') @Post('/login')
async login( async login(
@Body(ValidationPipe) loginCredential: LoginCredentialDto, @Body(new ValidationPipe({ transform: true })) loginCredential: LoginCredentialDto,
@Res() response: Response, @Res() response: Response,
): Promise<LoginResponseDto> { ): Promise<LoginResponseDto> {
const loginResponse = await this.authService.login(loginCredential); const loginResponse = await this.authService.login(loginCredential);
@@ -42,7 +42,9 @@ export class AuthController {
@Post('/admin-sign-up') @Post('/admin-sign-up')
@ApiBadRequestResponse({ description: 'The server already has an admin' }) @ApiBadRequestResponse({ description: 'The server already has an admin' })
async adminSignUp(@Body(ValidationPipe) signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> { async adminSignUp(
@Body(new ValidationPipe({ transform: true })) signUpCredential: SignUpDto,
): Promise<AdminSignupResponseDto> {
return await this.authService.adminSignUp(signUpCredential); return await this.authService.adminSignUp(signUpCredential);
} }
@@ -61,8 +63,7 @@ export class AuthController {
const status = new LogoutResponseDto(true); const status = new LogoutResponseDto(true);
response.send(status) response.send(status);
return status; return status;
} }
} }

View File

@@ -1,9 +1,11 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsNotEmpty } from 'class-validator'; import { IsNotEmpty } from 'class-validator';
export class LoginCredentialDto { export class LoginCredentialDto {
@IsNotEmpty() @IsNotEmpty()
@ApiProperty({ example: 'testuser@email.com' }) @ApiProperty({ example: 'testuser@email.com' })
@Transform(({ value }) => value?.toLowerCase())
email!: string; email!: string;
@IsNotEmpty() @IsNotEmpty()

View File

@@ -1,9 +1,11 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsNotEmpty, IsEmail } from 'class-validator'; import { IsNotEmpty, IsEmail } from 'class-validator';
export class SignUpDto { export class SignUpDto {
@IsEmail() @IsEmail()
@ApiProperty({ example: 'testuser@email.com' }) @ApiProperty({ example: 'testuser@email.com' })
@Transform(({ value }) => value?.toLowerCase())
email!: string; email!: string;
@IsNotEmpty() @IsNotEmpty()

View File

@@ -1,8 +1,10 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsNotEmpty, IsEmail } from 'class-validator'; import { IsNotEmpty, IsEmail } from 'class-validator';
export class CreateUserDto { export class CreateUserDto {
@IsEmail() @IsEmail()
@Transform(({ value }) => value?.toLowerCase())
@ApiProperty({ example: 'testuser@email.com' }) @ApiProperty({ example: 'testuser@email.com' })
email!: string; email!: string;

View File

@@ -61,7 +61,9 @@ export class UserController {
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminRolesGuard) @UseGuards(AdminRolesGuard)
@Post() @Post()
async createUser(@Body(ValidationPipe) createUserDto: CreateUserDto): Promise<UserResponseDto> { async createUser(
@Body(new ValidationPipe({ transform: true })) createUserDto: CreateUserDto,
): Promise<UserResponseDto> {
return await this.userService.createUser(createUserDto); return await this.userService.createUser(createUserDto);
} }

View File

@@ -10,7 +10,7 @@ export interface IServerVersion {
export const serverVersion: IServerVersion = { export const serverVersion: IServerVersion = {
major: 1, major: 1,
minor: 22, minor: 23,
patch: 0, patch: 0,
build: 0, build: 0,
}; };

View File

@@ -8,8 +8,6 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
</script> </script>
<p class="text-sm">USER LIST</p>
<table class="text-left w-full my-4"> <table class="text-left w-full my-4">
<thead class="border rounded-md mb-2 bg-gray-50 flex text-immich-primary w-full h-12 "> <thead class="border rounded-md mb-2 bg-gray-50 flex text-immich-primary w-full h-12 ">
<tr class="flex w-full place-items-center"> <tr class="flex w-full place-items-center">

View File

@@ -1,208 +1,207 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import AsserViewerNavBar from './asser-viewer-nav-bar.svelte'; import AsserViewerNavBar from './asser-viewer-nav-bar.svelte';
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte'; import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte'; import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
import PhotoViewer from './photo-viewer.svelte'; import PhotoViewer from './photo-viewer.svelte';
import DetailPanel from './detail-panel.svelte'; import DetailPanel from './detail-panel.svelte';
import { downloadAssets } from '$lib/stores/download'; import { downloadAssets } from '$lib/stores/download';
import VideoViewer from './video-viewer.svelte'; import VideoViewer from './video-viewer.svelte';
import { api, AssetResponseDto, AssetTypeEnum } from '@api'; import { api, AssetResponseDto, AssetTypeEnum } from '@api';
import { browser } from '$app/env'; import { browser } from '$app/env';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
let halfLeftHover = false; let halfLeftHover = false;
let halfRightHover = false; let halfRightHover = false;
let isShowDetail = false; let isShowDetail = false;
onMount(() => { onMount(() => {
if (browser) { if (browser) {
document.addEventListener('keydown', (keyInfo) => handleKeyboardPress(keyInfo.key)); document.addEventListener('keydown', (keyInfo) => handleKeyboardPress(keyInfo.key));
} }
}); });
const handleKeyboardPress = (key: string) => { const handleKeyboardPress = (key: string) => {
switch (key) { switch (key) {
case 'Escape': case 'Escape':
closeViewer(); closeViewer();
return; return;
case 'i': case 'i':
isShowDetail = !isShowDetail; isShowDetail = !isShowDetail;
return; return;
case 'ArrowLeft': case 'ArrowLeft':
navigateAssetBackward(); navigateAssetBackward();
return; return;
case 'ArrowRight': case 'ArrowRight':
navigateAssetForward(); navigateAssetForward();
return; return;
} }
}; };
const closeViewer = () => { const closeViewer = () => {
dispatch('close'); dispatch('close');
}; };
const navigateAssetForward = (e?: Event) => { const navigateAssetForward = (e?: Event) => {
e?.stopPropagation(); e?.stopPropagation();
dispatch('navigate-forward'); dispatch('navigate-forward');
}; };
const navigateAssetBackward = (e?: Event) => { const navigateAssetBackward = (e?: Event) => {
e?.stopPropagation(); e?.stopPropagation();
dispatch('navigate-backward'); dispatch('navigate-backward');
}; };
const showDetailInfoHandler = () => { const showDetailInfoHandler = () => {
isShowDetail = !isShowDetail; isShowDetail = !isShowDetail;
}; };
const downloadFile = async () => { const downloadFile = async () => {
try { try {
const imageName = asset.exifInfo?.imageName ? asset.exifInfo?.imageName : asset.id; console.log(asset.exifInfo);
const imageExtension = asset.originalPath.split('.')[1]; const imageName = asset.exifInfo?.imageName ? asset.exifInfo?.imageName : asset.id;
const imageFileName = imageName + '.' + imageExtension; const imageExtension = asset.originalPath.split('.')[1];
const imageFileName = imageName + '.' + imageExtension;
// If assets is already download -> return; // If assets is already download -> return;
if ($downloadAssets[imageFileName]) { if ($downloadAssets[imageFileName]) {
return; return;
} }
$downloadAssets[imageFileName] = 0; $downloadAssets[imageFileName] = 0;
const { data, status } = await api.assetApi.downloadFile( const {data, status} = await api.assetApi.downloadFile(
asset.deviceAssetId, asset.deviceAssetId,
asset.deviceId, asset.deviceId,
false, false,
false, false,
{ {
responseType: 'blob', responseType: 'blob',
onDownloadProgress: (progressEvent) => { onDownloadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) { if (progressEvent.lengthComputable) {
const total = progressEvent.total; const total = progressEvent.total;
const current = progressEvent.loaded; const current = progressEvent.loaded;
let percentCompleted = Math.floor((current / total) * 100); $downloadAssets[imageFileName] = Math.floor((current / total) * 100);
}
}
}
);
$downloadAssets[imageFileName] = percentCompleted; if (!(data instanceof Blob)) {
} return;
} }
}
);
if (!(data instanceof Blob)) { if (status === 200) {
return; const fileUrl = URL.createObjectURL(data);
} const anchor = document.createElement('a');
anchor.href = fileUrl;
anchor.download = imageFileName;
if (status === 200) { document.body.appendChild(anchor);
const fileUrl = URL.createObjectURL(data); anchor.click();
const anchor = document.createElement('a'); document.body.removeChild(anchor);
anchor.href = fileUrl;
anchor.download = imageFileName;
document.body.appendChild(anchor); URL.revokeObjectURL(fileUrl);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(fileUrl); // Remove item from download list
setTimeout(() => {
// Remove item from download list const copy = $downloadAssets;
setTimeout(() => { delete copy[imageFileName];
const copy = $downloadAssets; $downloadAssets = copy;
delete copy[imageFileName]; }, 2000);
$downloadAssets = copy; }
}, 2000); } catch (e) {
} console.log('Error downloading file ', e);
} catch (e) { }
console.log('Error downloading file ', e); };
}
};
</script> </script>
<section <section
id="immich-asset-viewer" id="immich-asset-viewer"
class="fixed h-screen w-screen top-0 overflow-y-hidden bg-black z-[999] grid grid-rows-[64px_1fr] grid-cols-4 " class="fixed h-screen w-screen top-0 overflow-y-hidden bg-black z-[999] grid grid-rows-[64px_1fr] grid-cols-4 "
> >
<div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform"> <div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform">
<AsserViewerNavBar <AsserViewerNavBar
on:goBack={closeViewer} on:goBack={closeViewer}
on:showDetail={showDetailInfoHandler} on:showDetail={showDetailInfoHandler}
on:download={downloadFile} on:download={downloadFile}
/> />
</div> </div>
<div <div
class={`row-start-2 row-span-end col-start-1 col-span-2 flex place-items-center hover:cursor-pointer w-3/4 ${ class={`row-start-2 row-span-end col-start-1 col-span-2 flex place-items-center hover:cursor-pointer w-3/4 ${
asset.type == AssetTypeEnum.Video ? '' : 'z-[999]' asset.type === AssetTypeEnum.Video ? '' : 'z-[999]'
}`} }`}
on:mouseenter={() => { on:mouseenter={() => {
halfLeftHover = true; halfLeftHover = true;
halfRightHover = false; halfRightHover = false;
}} }}
on:mouseleave={() => { on:mouseleave={() => {
halfLeftHover = false; halfLeftHover = false;
}} }}
on:click={navigateAssetBackward} on:click={navigateAssetBackward}
> >
<button <button
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 z-[1000] text-gray-500 mx-4" class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 z-[1000] text-gray-500 mx-4"
class:navigation-button-hover={halfLeftHover} class:navigation-button-hover={halfLeftHover}
on:click={navigateAssetBackward} on:click={navigateAssetBackward}
> >
<ChevronLeft size="36" /> <ChevronLeft size="36"/>
</button> </button>
</div> </div>
<div class="row-start-1 row-span-full col-start-1 col-span-4"> <div class="row-start-1 row-span-full col-start-1 col-span-4">
{#key asset.id} {#key asset.id}
{#if asset.type == AssetTypeEnum.Image} {#if asset.type === AssetTypeEnum.Image}
<PhotoViewer assetId={asset.id} deviceId={asset.deviceId} on:close={closeViewer} /> <PhotoViewer assetId={asset.id} deviceId={asset.deviceId} on:close={closeViewer}/>
{:else} {:else}
<VideoViewer assetId={asset.id} on:close={closeViewer} /> <VideoViewer assetId={asset.id} on:close={closeViewer}/>
{/if} {/if}
{/key} {/key}
</div> </div>
<div <div
class={`row-start-2 row-span-full col-start-3 col-span-2 flex justify-end place-items-center hover:cursor-pointer w-3/4 justify-self-end ${ class={`row-start-2 row-span-full col-start-3 col-span-2 flex justify-end place-items-center hover:cursor-pointer w-3/4 justify-self-end ${
asset.type == AssetTypeEnum.Video ? '' : 'z-[500]' asset.type === AssetTypeEnum.Video ? '' : 'z-[500]'
}`} }`}
on:click={navigateAssetForward} on:click={navigateAssetForward}
on:mouseenter={() => { on:mouseenter={() => {
halfLeftHover = false; halfLeftHover = false;
halfRightHover = true; halfRightHover = true;
}} }}
on:mouseleave={() => { on:mouseleave={() => {
halfRightHover = false; halfRightHover = false;
}} }}
> >
<button <button
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4 z-[1000]" class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4 z-[1000]"
class:navigation-button-hover={halfRightHover} class:navigation-button-hover={halfRightHover}
on:click={navigateAssetForward} on:click={navigateAssetForward}
> >
<ChevronRight size="36" /> <ChevronRight size="36"/>
</button> </button>
</div> </div>
{#if isShowDetail} {#if isShowDetail}
<div <div
transition:fly={{ duration: 150 }} transition:fly={{ duration: 150 }}
id="detail-panel" id="detail-panel"
class="bg-immich-bg w-[360px] row-span-full transition-all " class="bg-immich-bg w-[360px] row-span-full transition-all "
translate="yes" translate="yes"
> >
<DetailPanel {asset} on:close={() => (isShowDetail = false)} /> <DetailPanel {asset} on:close={() => (isShowDetail = false)}/>
</div> </div>
{/if} {/if}
</section> </section>
<style> <style>
.navigation-button-hover { .navigation-button-hover {
background-color: rgb(107 114 128 / var(--tw-bg-opacity)); background-color: rgb(107 114 128 / var(--tw-bg-opacity));
color: rgb(55 65 81 / var(--tw-text-opacity)); color: rgb(55 65 81 / var(--tw-text-opacity));
transition: all 150ms; transition: all 150ms;
} }
</style> </style>

View File

@@ -47,7 +47,7 @@
<div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8"> <div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8">
<div class="flex flex-col place-items-center place-content-center gap-4 px-4"> <div class="flex flex-col place-items-center place-content-center gap-4 px-4">
<img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo" /> <img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo" />
<h1 class="text-2xl text-immich-primary font-medium">Chage Password</h1> <h1 class="text-2xl text-immich-primary font-medium">Change Password</h1>
<p class="text-sm border rounded-md p-4 font-mono text-gray-600"> <p class="text-sm border rounded-md p-4 font-mono text-gray-600">
Hi {user.firstName} Hi {user.firstName}

View File

@@ -1,103 +1,121 @@
<script lang="ts"> <script lang="ts">
import { api, UserResponseDto } from '@api'; import { api, UserResponseDto } from '@api';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import AccountEditOutline from 'svelte-material-icons/AccountEditOutline.svelte'; import AccountEditOutline from 'svelte-material-icons/AccountEditOutline.svelte';
export let user: UserResponseDto; export let user: UserResponseDto;
let error: string; let error: string;
let success: string; let success: string;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const editUser = async (event: SubmitEvent) => { const editUser = async (event: SubmitEvent) => {
try {
const formElement = event.target as HTMLFormElement;
const form = new FormData(formElement);
const formElement = event.target as HTMLFormElement; const firstName = form.get('firstName');
const form = new FormData(formElement); const lastName = form.get('lastName');
const firstName = form.get('firstName'); const { status } = await api.userApi.updateUser({
const lastName = form.get('lastName'); id: user.id,
firstName: firstName!.toString(),
lastName: lastName!.toString()
});
if (status == 200) {
dispatch('edit-success');
}
} catch (e) {
console.log('Error updating user ', e);
}
};
const {status} = await api.userApi.updateUser({ const resetPassword = async () => {
id: user.id, try {
firstName: firstName.toString(), if (window.confirm('Do you want to reset the user password?')) {
lastName: lastName.toString() const defaultPassword = 'password';
}).catch(e => console.log("Error updating user ", e));
if (status == 200) { const { status } = await api.userApi.updateUser({
dispatch('edit-success'); id: user.id,
} password: defaultPassword,
} shouldChangePassword: true
});
const resetPassword = async () => { if (status == 200) {
const defaultPassword = 'password' dispatch('reset-password-success');
}
const {status} = await api.userApi.updateUser({ }
id: user.id, } catch (e) {
password: defaultPassword, console.log('Error reseting user password', e);
shouldChangePassword: true, }
};
}).catch(e => console.log("Error updating user ", e));
if (status == 200) {
dispatch('reset-password-success');
}
}
</script> </script>
<div class="border bg-white p-4 shadow-sm w-[500px] rounded-3xl py-8"> <div class="border bg-white p-4 shadow-sm w-[500px] rounded-3xl py-8">
<div class="flex flex-col place-items-center place-content-center gap-4 px-4"> <div class="flex flex-col place-items-center place-content-center gap-4 px-4">
<!-- <img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo"/>--> <!-- <img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo"/>-->
<AccountEditOutline size="4em" color="#4250affe"/> <AccountEditOutline size="4em" color="#4250affe" />
<h1 class="text-2xl text-immich-primary font-medium">Edit user</h1> <h1 class="text-2xl text-immich-primary font-medium">Edit user</h1>
</div> </div>
<form on:submit|preventDefault={editUser} autocomplete="off"> <form on:submit|preventDefault={editUser} autocomplete="off">
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Email <label class="immich-form-label" for="email">Email (cannot change)</label>
(cannot change)</label> <input
<input class="immich-form-input disabled:bg-gray-200 hover:cursor-not-allowed" class="immich-form-input disabled:bg-gray-200 hover:cursor-not-allowed"
id="email" name="email" id="email"
type="email" disabled name="email"
bind:value={user.email}/> type="email"
</div> disabled
bind:value={user.email}
/>
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="firstName">First Name</label>
<input
class="immich-form-input"
id="firstName"
name="firstName"
type="text"
required
bind:value={user.firstName}
/>
</div>
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="firstName">First Name</label> <label class="immich-form-label" for="lastName">Last Name</label>
<input class="immich-form-input" id="firstName" name="firstName" type="text" required <input
bind:value={user.firstName}/> class="immich-form-input"
</div> id="lastName"
name="lastName"
type="text"
required
bind:value={user.lastName}
/>
</div>
<div class="m-4 flex flex-col gap-2"> {#if error}
<label class="immich-form-label" for="lastName">Last Name</label> <p class="text-red-400 ml-4 text-sm">{error}</p>
<input class="immich-form-input" id="lastName" name="lastName" type="text" required {/if}
bind:value={user.lastName}/>
</div>
{#if success}
{#if error} <p class="text-immich-primary ml-4 text-sm">{success}</p>
<p class="text-red-400 ml-4 text-sm">{error}</p> {/if}
{/if} <div class="flex w-full px-4 gap-4 mt-8">
<button
{#if success} on:click={resetPassword}
<p class="text-immich-primary ml-4 text-sm">{success}</p> class="flex-1 transition-colors bg-[#F9DEDC] hover:bg-red-50 text-[#410E0B] px-6 py-3 rounded-full w-full font-medium"
{/if} >Reset password
<div class="flex w-full px-4 gap-4 mt-8"> </button>
<button on:click={resetPassword} <button
class="flex-1 transition-colors bg-[#F9DEDC] hover:bg-red-50 text-[#410E0B] px-6 py-3 rounded-full w-full font-medium" type="submit"
class="flex-1 transition-colors bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
>Reset password >Confirm
</button </button>
> </div>
<button </form>
type="submit"
class="flex-1 transition-colors bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
>Confirm
</button
>
</div>
</form>
</div> </div>

View File

@@ -63,7 +63,7 @@ async function fileUploader(asset: File, uploadType: UploadType) {
let exifData = null; let exifData = null;
if (assetType !== 'VIDEO') { if (assetType !== 'VIDEO') {
exifData = await exifr.parse(asset); exifData = await exifr.parse(asset).catch((e) => console.log('error parsing exif', e));
} }
const createdAt = const createdAt =

View File

@@ -1,181 +1,181 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { api, UserResponseDto } from '@api'; import { api, UserResponseDto } from '@api';
import { browser } from '$app/env'; import { browser } from '$app/env';
export const load: Load = async ({fetch, session}) => { export const load: Load = async ({ fetch, session }) => {
if (!browser && !session.user) { if (!browser && !session.user) {
return { return {
status: 302, status: 302,
redirect: '/auth/login' redirect: '/auth/login'
}; };
} }
try { try {
const user: UserResponseDto = await fetch('/data/user/get-my-user-info').then((r) =>
r.json()
);
const allUsers: UserResponseDto[] = await fetch('/data/user/get-all-users?isAll=false').then(
(r) => r.json()
);
if (!user.isAdmin) {
return {
status: 302,
redirect: '/photos'
};
}
const user: UserResponseDto = await fetch('/data/user/get-my-user-info').then((r) => r.json()); return {
const allUsers: UserResponseDto[] = await fetch<UserResponseDto[]>('/data/user/get-all-users?isAll=false').then((r) => r.json()); status: 200,
props: {
if (!user.isAdmin) { user: user,
return { allUsers: allUsers
status: 302, }
redirect: '/photos' };
}; } catch (e) {
} return {
status: 302,
return { redirect: '/auth/login'
status: 200, };
props: { }
user: user, };
allUsers: allUsers
}
};
} catch (e) {
return {
status: 302,
redirect: '/auth/login'
};
}
};
</script> </script>
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { AdminSideBarSelection } from '$lib/models/admin-sidebar-selection'; import { AdminSideBarSelection } from '$lib/models/admin-sidebar-selection';
import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte'; import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte'; import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte'; import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
import UserManagement from '$lib/components/admin-page/user-management.svelte'; import UserManagement from '$lib/components/admin-page/user-management.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import CreateUserForm from '$lib/components/forms/create-user-form.svelte'; import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
import EditUserForm from '$lib/components/forms/edit-user-form.svelte'; import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
import StatusBox from '$lib/components/shared-components/status-box.svelte'; import StatusBox from '$lib/components/shared-components/status-box.svelte';
let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT; export let user: UserResponseDto;
export let allUsers: UserResponseDto[];
export let user: UserResponseDto; let editUser: UserResponseDto;
export let allUsers: UserResponseDto[];
let editUser: UserResponseDto; let shouldShowEditUserForm = false;
let shouldShowCreateUserForm = false;
let shouldShowInfoPanel = false;
let shouldShowEditUserForm = false; const onButtonClicked = (buttonType: CustomEvent) => {
let shouldShowCreateUserForm = false; selectedAction = buttonType.detail['actionType'] as AdminSideBarSelection;
let shouldShowInfoPanel = false; };
const onButtonClicked = (buttonType: CustomEvent) => { onMount(() => {
selectedAction = buttonType.detail['actionType'] as AdminSideBarSelection; selectedAction = AdminSideBarSelection.USER_MANAGEMENT;
}; });
onMount(() => { const onUserCreated = async () => {
selectedAction = AdminSideBarSelection.USER_MANAGEMENT; const { data } = await api.userApi.getAllUsers(false);
}); allUsers = data;
shouldShowCreateUserForm = false;
};
const onUserCreated = async () => { const editUserHandler = async (event: CustomEvent) => {
const {data} = await api.userApi.getAllUsers(false); const { user } = event.detail;
allUsers = data; editUser = user;
shouldShowCreateUserForm = false; shouldShowEditUserForm = true;
}; };
const editUserHandler = async (event: CustomEvent) => { const onEditUserSuccess = async () => {
const {user} = event.detail; const { data } = await api.userApi.getAllUsers(false);
editUser = user; allUsers = data;
shouldShowEditUserForm = true; shouldShowEditUserForm = false;
}; };
const onEditUserSuccess = async () => { const onEditPasswordSuccess = async () => {
const {data} = await api.userApi.getAllUsers(false); const { data } = await api.userApi.getAllUsers(false);
allUsers = data; allUsers = data;
shouldShowEditUserForm = false; shouldShowEditUserForm = false;
} shouldShowInfoPanel = true;
};
const onEditPasswordSuccess = async () => {
const {data} = await api.userApi.getAllUsers(false);
allUsers = data;
shouldShowEditUserForm = false;
shouldShowInfoPanel = true;
}
</script> </script>
<svelte:head> <svelte:head>
<title>Administration - Immich</title> <title>Administration - Immich</title>
</svelte:head> </svelte:head>
<NavigationBar {user}/> <NavigationBar {user} />
{#if shouldShowCreateUserForm} {#if shouldShowCreateUserForm}
<FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}> <FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
<CreateUserForm on:user-created={onUserCreated}/> <CreateUserForm on:user-created={onUserCreated} />
</FullScreenModal> </FullScreenModal>
{/if} {/if}
{#if shouldShowEditUserForm} {#if shouldShowEditUserForm}
<FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}> <FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
<EditUserForm user={editUser} on:edit-success={onEditUserSuccess} <EditUserForm
on:reset-password-success={onEditPasswordSuccess}/> user={editUser}
</FullScreenModal> on:edit-success={onEditUserSuccess}
on:reset-password-success={onEditPasswordSuccess}
/>
</FullScreenModal>
{/if} {/if}
{#if shouldShowInfoPanel} {#if shouldShowInfoPanel}
<FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}> <FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
<div class="border bg-white shadow-sm w-[500px] rounded-3xl p-8 text-sm">
<h1 class="font-bold text-immich-primary text-lg mb-4">Password reset success</h1>
<div class="border bg-white shadow-sm w-[500px] rounded-3xl p-8 text-sm"> <p>
<h1 class="font-bold text-immich-primary text-lg mb-4">Password reset success</h1> The user's password has been reset to the default <code
class="font-bold bg-gray-200 px-2 py-1 rounded-md text-immich-primary">password</code
>
<br />
Please inform the user, and they will need to change the password at the next log-on.
</p>
<p> <div class="flex w-full">
The user's password has been reset to the default <code <button
class="font-bold bg-gray-200 px-2 py-1 rounded-md text-immich-primary">password</code> on:click={() => (shouldShowInfoPanel = false)}
<br> class="mt-6 bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
Please inform the user, and they will need to change the password at the next log-on. >Done
</p> </button>
</div>
<div class="flex w-full"> </div>
<button </FullScreenModal>
on:click={() => shouldShowInfoPanel = false}
class="mt-6 bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
>Done
</button
>
</div>
</div>
</FullScreenModal>
{/if} {/if}
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen"> <section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen">
<section id="admin-sidebar" class="pt-8 pr-6 flex flex-col"> <section id="admin-sidebar" class="pt-8 pr-6 flex flex-col">
<SideBarButton <SideBarButton
title="User" title="User"
logo={AccountMultipleOutline} logo={AccountMultipleOutline}
actionType={AdminSideBarSelection.USER_MANAGEMENT} actionType={AdminSideBarSelection.USER_MANAGEMENT}
isSelected={selectedAction === AdminSideBarSelection.USER_MANAGEMENT} isSelected={selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
on:selected={onButtonClicked} on:selected={onButtonClicked}
/> />
<div class="mb-6 mt-auto"> <div class="mb-6 mt-auto">
<StatusBox/> <StatusBox />
</div> </div>
</section> </section>
<section class="overflow-y-auto relative"> <section class="overflow-y-auto relative">
<div id="setting-title" class="pt-10 fixed w-full z-50 bg-immich-bg"> <div id="setting-title" class="pt-10 fixed w-full z-50 bg-immich-bg">
<h1 class="text-lg ml-8 mb-4 text-immich-primary font-medium">{selectedAction}</h1> <h1 class="text-lg ml-8 mb-4 text-immich-primary font-medium">{selectedAction}</h1>
<hr/> <hr />
</div> </div>
<section id="setting-content" class="relative pt-[85px] flex place-content-center">
<section class="w-[800px] pt-4">
{#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
<UserManagement
{allUsers}
on:create-user={() => (shouldShowCreateUserForm = true)}
on:edit-user={editUserHandler}
/>
{/if}
</section>
</section>
</section>
<section id="setting-content" class="relative pt-[85px] flex place-content-center">
<section class="w-[800px] pt-4">
{#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
<UserManagement
{allUsers}
on:create-user={() => (shouldShowCreateUserForm = true)}
on:edit-user={editUserHandler}
/>
{/if}
</section>
</section>
</section>
</section> </section>

View File

@@ -39,12 +39,9 @@
export let user: UserResponseDto; export let user: UserResponseDto;
const onSuccessHandler = async () => { const onSuccessHandler = async () => {
/** Svelte route fetch */ await fetch('auth/logout', { method: 'POST' });
const res = await fetch('/auth/logout', { method: 'POST' });
goto('/auth/login');
if (res.status == 200 && res.statusText == 'OK') {
goto('/auth/login');
}
}; };
</script> </script>