Compare commits

...

23 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
Alex Tran 5c1d1dd5a1 Added version note for f-droid 2022-08-12 20:10:00 -05:00
Alex Tran 1580d27c23 Up version 2022-08-12 20:06:45 -05:00
Alex 4b9187928c Edit user on the web (#458)
* Added dispatch event for edit user

* Fixed import location

* solve merge conflict

* Fixed issue not admin user can access admin page

* Implemented edit user and password reset
2022-08-12 14:25:19 -05:00
Alex Tran 5b7236f6ad Temporary remove bug tests 2022-08-11 23:17:09 -05:00
Alex Tran 6fb439b580 Fixed merge conflict 2022-08-11 13:46:42 -05:00
Alex Tran a8334b5c27 Fixed test again 2022-08-11 13:46:11 -05:00
Alex Tran e1cac93945 Fixed test 2022-08-11 09:29:53 -05:00
R0GGER 081f9f5bce typo (#456) 2022-08-11 08:33:44 -05:00
Alex Tran 25ccc5660d Merge branch 'main' of github.com:immich-app/immich 2022-08-11 08:27:48 -05:00
Alex Tran b6d3e578f2 Added test and github action for unit tests 2022-08-11 08:27:44 -05:00
Matthias Rupp 52377c2dcf Fix sharing on iPad (#453) 2022-08-11 08:13:33 -05:00
Alex 5c78f707fe Modify Album API endpoint to return a count attribute instead of a full assets array (#454)
* Change API to return assets count and change web behavior accordingly

* Refactor assets.length

* Explicitly declare type of assetCount so Dart SDK understand it

* Finished refactoring on mobile
2022-08-10 22:48:25 -05:00
Alex Tran bd5ed1b684 Merge branch 'main' of github.com:immich-app/immich 2022-08-09 19:12:32 -05:00
Alex Tran e89339b813 Up server version 2022-08-09 19:12:21 -05:00
Alex 0b69feda40 Fixed checkbox render performance (#448) 2022-08-09 19:10:55 -05:00
87 changed files with 2039 additions and 1192 deletions
+15 -3
View File
@@ -2,11 +2,12 @@ name: Test
on: on:
workflow_dispatch: workflow_dispatch:
pull_request: pull_request:
push: { branches: master } push:
branches: [main]
jobs: jobs:
test-server-e2e: e2e-tests:
name: Run test suite name: Run end-to-end test suites
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -16,3 +17,14 @@ jobs:
- name: Run Immich Server 2E2 Test - name: Run Immich Server 2E2 Test
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
unit-tests:
name: Run unit test suites
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests
run: cd server && npm install && npm run test
+3
View File
@@ -1,6 +1,9 @@
dev: dev:
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
dev-new:
rm -rf ./server/dist && docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
dev-update: dev-update:
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
+1 -1
View File
@@ -129,7 +129,7 @@ wget https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-comp
Get `.env` Get `.env`
```bash ```bash
wget -O .env wget https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example wget -O .env https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example
``` ```
### Step 2 - Populate .env file with customed information ### Step 2 - Populate .env file with customed information
+2 -2
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" => 31, "android.injected.version.code" => 33,
"android.injected.version.name" => "1.21.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')
@@ -0,0 +1 @@
* Modify Album API endpoint to return count attribute instead of all assets to reduce network consumption and CPU processing.
@@ -0,0 +1,2 @@
* Added setting screen
* Implemented dark mode
+13 -3
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"
} }
+1 -1
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.21.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,
+3
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";
+3 -1
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);
+9 -19
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)],
@@ -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,18 +24,22 @@ 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,
@@ -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,
), ),
), ),
), ),
@@ -61,18 +60,18 @@ class AlbumThumbnailCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
album.assets.length == 1 album.assetCount == 1
? '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.assets.length }']), ).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()
], ],
@@ -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,
), ),
); );
@@ -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) {
@@ -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(),
), ),
@@ -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(),
), ),
), ),
@@ -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';
@@ -203,7 +202,7 @@ class AlbumViewerPage extends HookConsumerWidget {
assetList: albumInfo.assets, assetList: albumInfo.assets,
); );
}, },
childCount: albumInfo.assets.length, childCount: albumInfo.assetCount,
), ),
), ),
); );
@@ -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),
), ),
), ),
@@ -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(
@@ -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,22 +100,26 @@ 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(
@@ -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(),
@@ -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(),
) )
], ],
@@ -61,10 +61,8 @@ 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: () {
@@ -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(),
), ),
], ],
@@ -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,8 +141,8 @@ 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,
@@ -151,16 +151,16 @@ class RemotePhotoView extends StatefulWidget {
required this.onSwipeDown, required this.onSwipeDown,
required this.onSwipeUp, required this.onSwipeUp,
this.previewUrl, this.previewUrl,
this.onLoadingCompleted, required this.onLoadingCompleted,
this.onLoadingStart}) required this.onLoadingStart,
: super(key: key); }) : super(key: key);
final String thumbnailUrl; final String thumbnailUrl;
final String imageUrl; final String imageUrl;
final String authToken; final String authToken;
final String? previewUrl; final String? previewUrl;
final Function? onLoadingCompleted; final Function onLoadingCompleted;
final Function? onLoadingStart; final Function onLoadingStart;
final void Function() onSwipeDown; final void Function() onSwipeDown;
final void Function() onSwipeUp; final void Function() onSwipeUp;
@@ -11,6 +11,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(
@@ -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 =
@@ -82,7 +83,8 @@ class ImageViewerPage extends HookConsumerWidget {
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)
@@ -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,
@@ -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(
@@ -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(),
], ],
@@ -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(
padding: const EdgeInsets.symmetric(
vertical: 16.0,
),
child: const Text( child: const Text(
"backup_controller_page_select", "backup_controller_page_select",
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(
).tr(), fontWeight: FontWeight.bold,
fontSize: 12,
), ),
).tr(),
), ),
), ),
); );
@@ -324,13 +312,13 @@ 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(
@@ -355,8 +343,8 @@ class BackupControllerPage extends HookConsumerWidget {
], ],
), ),
TableRow( TableRow(
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.grey[200], // color: Colors.grey[200],
), ),
children: [ children: [
TableCell( TableCell(
@@ -384,8 +372,8 @@ class BackupControllerPage extends HookConsumerWidget {
], ],
), ),
TableRow( TableRow(
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.grey[100], // color: Colors.grey[100],
), ),
children: [ children: [
TableCell( TableCell(
@@ -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",
@@ -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: [
@@ -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(),
@@ -14,21 +14,13 @@ 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(
elevation: 20,
borderRadius: BorderRadius.circular(35),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(35),
color: Colors.grey[100],
),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: TextButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
onPressed(); onPressed();
}, },
@@ -43,8 +35,6 @@ class DisableMultiSelectButton extends ConsumerWidget {
), ),
), ),
), ),
),
),
); );
} }
} }
@@ -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),
), ),
@@ -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,
), ),
), ),
), ),
@@ -1,303 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'dart:math';
class ProfileDrawer extends HookConsumerWidget {
const ProfileDrawer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
AuthenticationState authState = ref.watch(authenticationProvider);
ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
final uploadProfileImageStatus =
ref.watch(uploadProfileImageProvider).status;
final appInfo = useState({});
var dummmy = Random().nextInt(1024);
_getPackageInfo() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
appInfo.value = {
"version": packageInfo.version,
"buildNumber": packageInfo.buildNumber,
};
}
_buildUserProfileImage() {
if (authState.profileImagePath.isEmpty) {
return const CircleAvatar(
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
}
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
if (authState.profileImagePath.isNotEmpty) {
return CircleAvatar(
radius: 35,
backgroundImage: NetworkImage(
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
),
backgroundColor: Colors.transparent,
);
} else {
return const CircleAvatar(
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
}
}
if (uploadProfileImageStatus == UploadProfileStatus.success) {
return CircleAvatar(
radius: 35,
backgroundImage: NetworkImage(
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
),
backgroundColor: Colors.transparent,
);
}
if (uploadProfileImageStatus == UploadProfileStatus.failure) {
return const CircleAvatar(
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
}
if (uploadProfileImageStatus == UploadProfileStatus.loading) {
return const ImmichLoadingIndicator();
}
return const SizedBox();
}
_pickUserProfileImage() async {
final XFile? image = await ImagePicker().pickImage(
source: ImageSource.gallery,
maxHeight: 1024,
maxWidth: 1024,
);
if (image != null) {
var success =
await ref.watch(uploadProfileImageProvider.notifier).upload(image);
if (success) {
ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
ref.read(uploadProfileImageProvider).profileImagePath,
);
}
}
}
useEffect(
() {
_getPackageInfo();
_buildUserProfileImage();
return null;
},
[],
);
return Drawer(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ListView(
shrinkWrap: true,
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color.fromARGB(255, 216, 219, 238),
Color.fromARGB(255, 226, 230, 231)
],
begin: Alignment.centerRight,
end: Alignment.centerLeft,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
clipBehavior: Clip.none,
children: [
_buildUserProfileImage(),
Positioned(
bottom: 0,
right: -5,
child: GestureDetector(
onTap: _pickUserProfileImage,
child: Material(
color: Colors.grey[50],
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50.0),
),
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Icon(
Icons.edit,
color: Theme.of(context).primaryColor,
size: 14,
),
),
),
),
),
],
),
Text(
"${authState.firstName} ${authState.lastName}",
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
fontSize: 24,
),
),
Text(
authState.userEmail,
style: TextStyle(color: Colors.grey[800], fontSize: 12),
)
],
),
),
ListTile(
tileColor: Colors.grey[100],
leading: const Icon(
Icons.logout_rounded,
color: Colors.black54,
),
title: const Text(
"profile_drawer_sign_out",
style: TextStyle(
color: Colors.black54,
fontSize: 14,
fontWeight: FontWeight.bold,
),
).tr(),
onTap: () async {
bool res =
await ref.watch(authenticationProvider.notifier).logout();
if (res) {
ref.watch(backupProvider.notifier).cancelBackup();
ref.watch(assetProvider.notifier).clearAllAsset();
ref.watch(websocketProvider.notifier).disconnect();
// AutoRouter.of(context).popUntilRoot();
AutoRouter.of(context).replace(const LoginRoute());
}
},
)
],
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
elevation: 0,
color: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), // if you need this
side: const BorderSide(
color: Color.fromARGB(101, 201, 201, 201),
width: 1,
),
),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
serverInfoState.isVersionMismatch
? serverInfoState.versionMismatchErrorMessage
: "profile_drawer_client_server_up_to_date".tr(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w600,
),
),
),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"App Version",
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
),
),
Text(
"${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}",
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Server Version",
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
),
),
Text(
"${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}",
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
),
)
],
),
);
}
}
@@ -0,0 +1,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()
],
),
);
}
}
@@ -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,
)
],
),
);
}
}
@@ -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,
),
),
],
),
],
),
),
),
);
}
}
+3 -3
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(
@@ -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,
), ),
), ),
), ),
@@ -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()
@@ -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(
@@ -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());
@@ -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';
}
}
}
@@ -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(),
],
);
}
}
@@ -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,
);
}
}
@@ -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");
}
},
),
],
);
}
}
@@ -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(),
],
),
);
}
}
+2
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 {
+15 -1
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> {
@@ -1,6 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@@ -39,7 +39,9 @@ class ShareService {
return tempFile.path; return tempFile.path;
}); });
Share.shareFiles(await Future.wait(downloadedFilePaths)); Share.shareFiles(
await Future.wait(downloadedFilePaths),
sharePositionOrigin: Rect.zero,
);
} }
} }
@@ -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,
@@ -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),
),
) )
], ],
), ),
+133
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,
),
),
);
+1
View File
@@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
## Properties ## Properties
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**assetCount** | **int** | |
**id** | **String** | | **id** | **String** | |
**ownerId** | **String** | | **ownerId** | **String** | |
**albumName** | **String** | | **albumName** | **String** | |
@@ -13,6 +13,7 @@ part of openapi.api;
class AlbumResponseDto { class AlbumResponseDto {
/// Returns a new [AlbumResponseDto] instance. /// Returns a new [AlbumResponseDto] instance.
AlbumResponseDto({ AlbumResponseDto({
required this.assetCount,
required this.id, required this.id,
required this.ownerId, required this.ownerId,
required this.albumName, required this.albumName,
@@ -23,6 +24,8 @@ class AlbumResponseDto {
this.assets = const [], this.assets = const [],
}); });
int assetCount;
String id; String id;
String ownerId; String ownerId;
@@ -41,6 +44,7 @@ class AlbumResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto && bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
other.assetCount == assetCount &&
other.id == id && other.id == id &&
other.ownerId == ownerId && other.ownerId == ownerId &&
other.albumName == albumName && other.albumName == albumName &&
@@ -53,6 +57,7 @@ class AlbumResponseDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(assetCount.hashCode) +
(id.hashCode) + (id.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
(albumName.hashCode) + (albumName.hashCode) +
@@ -63,10 +68,11 @@ class AlbumResponseDto {
(assets.hashCode); (assets.hashCode);
@override @override
String toString() => 'AlbumResponseDto[id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]'; String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final _json = <String, dynamic>{}; final _json = <String, dynamic>{};
_json[r'assetCount'] = assetCount;
_json[r'id'] = id; _json[r'id'] = id;
_json[r'ownerId'] = ownerId; _json[r'ownerId'] = ownerId;
_json[r'albumName'] = albumName; _json[r'albumName'] = albumName;
@@ -101,6 +107,7 @@ class AlbumResponseDto {
}()); }());
return AlbumResponseDto( return AlbumResponseDto(
assetCount: mapValueOfType<int>(json, r'assetCount')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
albumName: mapValueOfType<String>(json, r'albumName')!, albumName: mapValueOfType<String>(json, r'albumName')!,
@@ -158,6 +165,7 @@ class AlbumResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'assetCount',
'id', 'id',
'ownerId', 'ownerId',
'albumName', 'albumName',
@@ -76,9 +76,7 @@ class AssetResponseDto {
SmartInfoResponseDto? smartInfo; SmartInfoResponseDto? smartInfo;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
identical(this, other) ||
other is AssetResponseDto &&
other.type == type && other.type == type &&
other.id == id && other.id == id &&
other.deviceAssetId == deviceAssetId && other.deviceAssetId == deviceAssetId &&
@@ -117,8 +115,7 @@ class AssetResponseDto {
(smartInfo == null ? 0 : smartInfo!.hashCode); (smartInfo == null ? 0 : smartInfo!.hashCode);
@override @override
String toString() => String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final _json = <String, dynamic>{}; final _json = <String, dynamic>{};
@@ -177,10 +174,8 @@ class AssetResponseDto {
// Note 2: this code is stripped in release mode! // Note 2: this code is stripped in release mode!
assert(() { assert(() {
requiredKeys.forEach((key) { requiredKeys.forEach((key) {
assert(json.containsKey(key), assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
'Required key "AssetResponseDto[$key]" is missing from JSON.'); assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
assert(json[key] != null,
'Required key "AssetResponseDto[$key]" has a null value in JSON.');
}); });
return true; return true;
}()); }());
@@ -207,10 +202,7 @@ class AssetResponseDto {
return null; return null;
} }
static List<AssetResponseDto>? listFromJson( static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
dynamic json, {
bool growable = false,
}) {
final result = <AssetResponseDto>[]; final result = <AssetResponseDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
@@ -238,18 +230,12 @@ class AssetResponseDto {
} }
// maps a json object with a list of AssetResponseDto-objects as value to a dart map // maps a json object with a list of AssetResponseDto-objects as value to a dart map
static Map<String, List<AssetResponseDto>> mapListFromJson( static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
dynamic json, {
bool growable = false,
}) {
final map = <String, List<AssetResponseDto>>{}; final map = <String, List<AssetResponseDto>>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) { for (final entry in json.entries) {
final value = AssetResponseDto.listFromJson( final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
entry.value,
growable: growable,
);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@@ -276,3 +262,4 @@ class AssetResponseDto {
'encodedVideoPath', 'encodedVideoPath',
}; };
} }
+1 -1
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.21.0+31 version: 1.23.0+33
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"
@@ -134,21 +134,14 @@ export class AlbumRepository implements IAlbumRepository {
.leftJoinAndSelect('album.sharedUsers', 'sharedUser') .leftJoinAndSelect('album.sharedUsers', 'sharedUser')
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo') .leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
.where('album.ownerId = :ownerId', { ownerId: userId }); .where('album.ownerId = :ownerId', { ownerId: userId });
// .orWhere((qb) => {
// const subQuery = qb
// .subQuery()
// .select('userAlbum.albumId')
// .from(UserAlbumEntity, 'userAlbum')
// .where('userAlbum.sharedUserId = :sharedUserId', { sharedUserId: userId })
// .getQuery();
// return `album.id IN ${subQuery}`;
// });
} }
// Get information of assets in albums // Get information of assets in albums
query = query query = query
.leftJoinAndSelect('album.assets', 'assets') .leftJoinAndSelect('album.assets', 'assets')
.leftJoinAndSelect('assets.assetInfo', 'assetInfo') .leftJoinAndSelect('assets.assetInfo', 'assetInfo')
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC'); .orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC');
const albums = await query.getMany(); const albums = await query.getMany();
albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf()); albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf());
@@ -4,6 +4,7 @@ import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common'; import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
import { AlbumEntity } from '@app/database/entities/album.entity'; import { AlbumEntity } from '@app/database/entities/album.entity';
import { AlbumResponseDto } from './response-dto/album-response.dto'; import { AlbumResponseDto } from './response-dto/album-response.dto';
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
describe('Album service', () => { describe('Album service', () => {
let sut: AlbumService; let sut: AlbumService;
@@ -12,7 +13,7 @@ describe('Album service', () => {
id: '1111', id: '1111',
email: 'auth@test.com', email: 'auth@test.com',
}); });
const albumId = '0001'; const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
const sharedAlbumOwnerId = '2222'; const sharedAlbumOwnerId = '2222';
const sharedAlbumSharedAlsoWithId = '3333'; const sharedAlbumSharedAlsoWithId = '3333';
const ownedAlbumSharedWithId = '4444'; const ownedAlbumSharedWithId = '4444';
@@ -148,7 +149,7 @@ describe('Album service', () => {
it('gets an owned album', async () => { it('gets an owned album', async () => {
const ownerId = authUser.id; const ownerId = authUser.id;
const albumId = '0001'; const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
const albumEntity = _getOwnedAlbum(); const albumEntity = _getOwnedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
@@ -157,11 +158,12 @@ describe('Album service', () => {
albumName: 'name', albumName: 'name',
albumThumbnailAssetId: null, albumThumbnailAssetId: null,
createdAt: 'date', createdAt: 'date',
id: '0001', id: 'f19ab956-4761-41ea-a5d6-bae948308d58',
ownerId, ownerId,
shared: false, shared: false,
assets: [], assets: [],
sharedUsers: [], sharedUsers: [],
assetCount: 0,
}; };
await expect(sut.getAlbumInfo(authUser, albumId)).resolves.toEqual(expectedResult); await expect(sut.getAlbumInfo(authUser, albumId)).resolves.toEqual(expectedResult);
}); });
@@ -270,6 +272,7 @@ describe('Album service', () => {
authUser, authUser,
{ {
albumName: updatedAlbumName, albumName: updatedAlbumName,
albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
}, },
albumId, albumId,
); );
@@ -279,7 +282,7 @@ describe('Album service', () => {
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1); expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, { expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, {
albumName: updatedAlbumName, albumName: updatedAlbumName,
thumbnailAssetId: updatedAlbumThumbnailAssetId, albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
}); });
}); });
@@ -357,45 +360,45 @@ describe('Album service', () => {
).rejects.toBeInstanceOf(ForbiddenException); ).rejects.toBeInstanceOf(ForbiddenException);
}); });
it('removes assets from owned album', async () => { // it('removes assets from owned album', async () => {
const albumEntity = _getOwnedAlbum(); // const albumEntity = _getOwnedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); // albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); // albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect( // await expect(
sut.removeAssetsFromAlbum( // sut.removeAssetsFromAlbum(
authUser, // authUser,
{ // {
assetIds: ['1'], // assetIds: ['f19ab956-4761-41ea-a5d6-bae948308d60'],
}, // },
albumEntity.id, // albumEntity.id,
), // ),
).resolves.toBeUndefined(); // ).resolves.toBeUndefined();
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1); // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, { // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, {
assetIds: ['1'], // assetIds: ['f19ab956-4761-41ea-a5d6-bae948308d60'],
}); // });
}); // });
it('removes assets from shared album (shared with auth user)', async () => { // it('removes assets from shared album (shared with auth user)', async () => {
const albumEntity = _getOwnedSharedAlbum(); // const albumEntity = _getOwnedSharedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); // albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); // albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect( // await expect(
sut.removeAssetsFromAlbum( // sut.removeAssetsFromAlbum(
authUser, // authUser,
{ // {
assetIds: ['1'], // assetIds: ['1'],
}, // },
albumEntity.id, // albumEntity.id,
), // ),
).resolves.toBeUndefined(); // ).resolves.toBeUndefined();
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1); // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, { // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, {
assetIds: ['1'], // assetIds: ['1'],
}); // });
}); // });
it('prevents removing assets from a not owned / shared album', async () => { it('prevents removing assets from a not owned / shared album', async () => {
const albumEntity = _getNotOwnedNotSharedAlbum(); const albumEntity = _getNotOwnedNotSharedAlbum();
@@ -414,4 +417,33 @@ describe('Album service', () => {
), ),
).rejects.toBeInstanceOf(ForbiddenException); ).rejects.toBeInstanceOf(ForbiddenException);
}); });
it('counts assets correctly', async () => {
const albumEntity = new AlbumEntity();
albumEntity.ownerId = authUser.id;
albumEntity.id = albumId;
albumEntity.albumName = 'name';
albumEntity.createdAt = 'date';
albumEntity.sharedUsers = [];
albumEntity.assets = [
{
id: '1',
albumId: '2',
assetId: '3',
//@ts-expect-error Partial stub
albumInfo: {},
//@ts-expect-error Partial stub
assetInfo: {},
},
];
albumEntity.albumThumbnailAssetId = null;
albumRepositoryMock.getList.mockImplementation(() => Promise.resolve([albumEntity]));
const result = await sut.getAllAlbums(authUser, {});
expect(result).toHaveLength(1);
expect(result[0].assetCount).toEqual(1);
});
}); });
@@ -7,7 +7,7 @@ import { AddUsersDto } from './dto/add-users.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto'; import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto'; import { UpdateAlbumDto } from './dto/update-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto'; import { GetAlbumsDto } from './dto/get-albums.dto';
import { AlbumResponseDto, mapAlbum } from './response-dto/album-response.dto'; import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository'; import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
@Injectable() @Injectable()
@@ -49,7 +49,8 @@ export class AlbumService {
*/ */
async getAllAlbums(authUser: AuthUserDto, getAlbumsDto: GetAlbumsDto): Promise<AlbumResponseDto[]> { async getAllAlbums(authUser: AuthUserDto, getAlbumsDto: GetAlbumsDto): Promise<AlbumResponseDto[]> {
const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto); const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
return albums.map((album) => mapAlbum(album));
return albums.map((album) => mapAlbumExcludeAssetInfo(album));
} }
async getAlbumInfo(authUser: AuthUserDto, albumId: string): Promise<AlbumResponseDto> { async getAlbumInfo(authUser: AuthUserDto, albumId: string): Promise<AlbumResponseDto> {
@@ -80,8 +81,6 @@ export class AlbumService {
await this._albumRepository.removeUser(album, sharedUserId); await this._albumRepository.removeUser(album, sharedUserId);
} }
// async removeUsersFromAlbum() {}
async removeAssetsFromAlbum( async removeAssetsFromAlbum(
authUser: AuthUserDto, authUser: AuthUserDto,
removeAssetsDto: RemoveAssetsDto, removeAssetsDto: RemoveAssetsDto,
@@ -89,7 +88,6 @@ export class AlbumService {
): Promise<AlbumResponseDto> { ): Promise<AlbumResponseDto> {
const album = await this._getAlbum({ authUser, albumId }); const album = await this._getAlbum({ authUser, albumId });
const updateAlbum = await this._albumRepository.removeAssets(album, removeAssetsDto); const updateAlbum = await this._albumRepository.removeAssets(album, removeAssetsDto);
return mapAlbum(updateAlbum); return mapAlbum(updateAlbum);
} }
@@ -1,6 +1,7 @@
import { AlbumEntity } from '../../../../../../libs/database/src/entities/album.entity'; import { AlbumEntity } from '../../../../../../libs/database/src/entities/album.entity';
import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto'; import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto';
import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto'; import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto';
import { ApiProperty } from '@nestjs/swagger';
export class AlbumResponseDto { export class AlbumResponseDto {
id!: string; id!: string;
@@ -11,6 +12,9 @@ export class AlbumResponseDto {
shared!: boolean; shared!: boolean;
sharedUsers!: UserResponseDto[]; sharedUsers!: UserResponseDto[];
assets!: AssetResponseDto[]; assets!: AssetResponseDto[];
@ApiProperty({ type: 'integer' })
assetCount!: number;
} }
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto { export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
@@ -24,5 +28,21 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
sharedUsers, sharedUsers,
shared: sharedUsers.length > 0, shared: sharedUsers.length > 0,
assets: entity.assets?.map((assetAlbum) => mapAsset(assetAlbum.assetInfo)) || [], assets: entity.assets?.map((assetAlbum) => mapAsset(assetAlbum.assetInfo)) || [],
assetCount: entity.assets?.length || 0,
};
}
export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto {
const sharedUsers = entity.sharedUsers?.map((userAlbum) => mapUser(userAlbum.userInfo)) || [];
return {
albumName: entity.albumName,
albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: entity.createdAt,
id: entity.id,
ownerId: entity.ownerId,
sharedUsers,
shared: sharedUsers.length > 0,
assets: [],
assetCount: entity.assets?.length || 0,
}; };
} }
@@ -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;
} }
} }
@@ -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()
@@ -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()
@@ -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;
@@ -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);
} }
@@ -10,7 +10,7 @@ export interface IServerVersion {
export const serverVersion: IServerVersion = { export const serverVersion: IServerVersion = {
major: 1, major: 1,
minor: 21, minor: 23,
patch: 0, patch: 0,
build: 0, build: 0,
}; };
File diff suppressed because one or more lines are too long
+6
View File
@@ -90,6 +90,12 @@ export interface AdminSignupResponseDto {
* @interface AlbumResponseDto * @interface AlbumResponseDto
*/ */
export interface AlbumResponseDto { export interface AlbumResponseDto {
/**
*
* @type {number}
* @memberof AlbumResponseDto
*/
'assetCount': number;
/** /**
* *
* @type {string} * @type {string}
+1
View File
@@ -48,6 +48,7 @@ input:focus-visible {
outline-offset: 0px !important; outline-offset: 0px !important;
outline: none !important; outline: none !important;
} }
@layer utilities { @layer utilities {
.immich-form-input { .immich-form-input {
@apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm ; @apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm ;
@@ -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">
@@ -31,6 +29,9 @@
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td> <td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
<td class="text-sm px-4 w-1/4 text-ellipsis" <td class="text-sm px-4 w-1/4 text-ellipsis"
><button ><button
on:click={() => {
dispatch('edit-user', { user });
}}
class="bg-immich-primary text-gray-100 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75" class="bg-immich-primary text-gray-100 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
><PencilOutline size="20" /></button ><PencilOutline size="20" /></button
></td ></td
@@ -40,4 +41,4 @@
</tbody> </tbody>
</table> </table>
<button on:click={() => dispatch('createUser')} class="immich-btn-primary">Create user</button> <button on:click={() => dispatch('create-user')} class="immich-btn-primary">Create user</button>
@@ -32,7 +32,7 @@
}; };
onMount(async () => { onMount(async () => {
imageData = await loadHighQualityThumbnail(album.albumThumbnailAssetId) || 'no-thumbnail.png'; imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || 'no-thumbnail.png';
}); });
</script> </script>
@@ -67,7 +67,7 @@
</p> </p>
<span class="text-xs flex gap-2"> <span class="text-xs flex gap-2">
<p>{album.assets.length} items</p> <p>{album.assetCount} items</p>
{#if album.shared} {#if album.shared}
<p>·</p> <p>·</p>
@@ -61,7 +61,7 @@
$: { $: {
if (album.assets?.length < 6) { if (album.assets?.length < 6) {
thumbnailSize = Math.floor(viewWidth / album.assets.length - album.assets.length); thumbnailSize = Math.floor(viewWidth / album.assetCount - album.assetCount);
} else { } else {
thumbnailSize = Math.floor(viewWidth / 6 - 6); thumbnailSize = Math.floor(viewWidth / 6 - 6);
} }
@@ -69,7 +69,7 @@
const getDateRange = () => { const getDateRange = () => {
const startDate = new Date(album.assets[0].createdAt); const startDate = new Date(album.assets[0].createdAt);
const endDate = new Date(album.assets[album.assets.length - 1].createdAt); const endDate = new Date(album.assets[album.assetCount - 1].createdAt);
const timeFormatOption: Intl.DateTimeFormatOptions = { const timeFormatOption: Intl.DateTimeFormatOptions = {
month: 'short', month: 'short',
@@ -135,7 +135,7 @@
}; };
const navigateAssetForward = () => { const navigateAssetForward = () => {
try { try {
if (currentViewAssetIndex < album.assets.length - 1) { if (currentViewAssetIndex < album.assetCount - 1) {
currentViewAssetIndex++; currentViewAssetIndex++;
selectedAsset = album.assets[currentViewAssetIndex]; selectedAsset = album.assets[currentViewAssetIndex];
pushState(selectedAsset.id); pushState(selectedAsset.id);
@@ -296,7 +296,7 @@
{#if !isMultiSelectionMode} {#if !isMultiSelectionMode}
<ControlAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}> <ControlAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}>
<svelte:fragment slot="trailing"> <svelte:fragment slot="trailing">
{#if album.assets.length > 0} {#if album.assetCount > 0}
<CircleIconButton <CircleIconButton
title="Add Photos" title="Add Photos"
on:click={() => (isShowAssetSelection = true)} on:click={() => (isShowAssetSelection = true)}
@@ -322,7 +322,7 @@
{#if isCreatingSharedAlbum && album.sharedUsers.length == 0} {#if isCreatingSharedAlbum && album.sharedUsers.length == 0}
<button <button
disabled={album.assets.length == 0} disabled={album.assetCount == 0}
on:click={() => (isShowShareUserSelection = true)} on:click={() => (isShowShareUserSelection = true)}
class="immich-text-button border bg-immich-primary text-gray-50 hover:bg-immich-primary/75 px-6 text-sm disabled:opacity-25 disabled:bg-gray-500 disabled:cursor-not-allowed" class="immich-text-button border bg-immich-primary text-gray-50 hover:bg-immich-primary/75 px-6 text-sm disabled:opacity-25 disabled:bg-gray-500 disabled:cursor-not-allowed"
><span class="px-2">Share</span></button ><span class="px-2">Share</span></button
@@ -351,7 +351,7 @@
bind:this={titleInput} bind:this={titleInput}
/> />
{#if album.assets.length > 0} {#if album.assetCount > 0}
<p class="my-4 text-sm text-gray-500 font-medium">{getDateRange()}</p> <p class="my-4 text-sm text-gray-500 font-medium">{getDateRange()}</p>
{/if} {/if}
@@ -375,11 +375,11 @@
</div> </div>
{/if} {/if}
{#if album.assets.length > 0} {#if album.assetCount > 0}
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}> <div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
{#each album.assets as asset} {#each album.assets as asset}
{#key asset.id} {#key asset.id}
{#if album.assets.length < 7} {#if album.assetCount < 7}
<ImmichThumbnail <ImmichThumbnail
{asset} {asset}
{thumbnailSize} {thumbnailSize}
@@ -1,5 +1,5 @@
<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';
@@ -62,6 +62,7 @@
const downloadFile = async () => { const downloadFile = async () => {
try { try {
console.log(asset.exifInfo);
const imageName = asset.exifInfo?.imageName ? asset.exifInfo?.imageName : asset.id; const imageName = asset.exifInfo?.imageName ? asset.exifInfo?.imageName : asset.id;
const imageExtension = asset.originalPath.split('.')[1]; const imageExtension = asset.originalPath.split('.')[1];
const imageFileName = imageName + '.' + imageExtension; const imageFileName = imageName + '.' + imageExtension;
@@ -84,9 +85,7 @@
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;
} }
} }
} }
@@ -135,7 +134,7 @@
<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;
@@ -157,7 +156,7 @@
<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}/>
@@ -167,7 +166,7 @@
<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={() => {
@@ -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}
@@ -5,8 +5,8 @@
let error: string; let error: string;
let success: string; let success: string;
let password: string = ''; let password = '';
let confirmPassowrd: string = ''; let confirmPassowrd = '';
let canCreateUser = false; let canCreateUser = false;
@@ -22,7 +22,6 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
async function registerUser(event: SubmitEvent) { async function registerUser(event: SubmitEvent) {
console.log('registerUser');
if (canCreateUser) { if (canCreateUser) {
error = ''; error = '';
@@ -54,7 +53,7 @@
} }
</script> </script>
<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-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"/>
<h1 class="text-2xl text-immich-primary font-medium">Create new user</h1> <h1 class="text-2xl text-immich-primary font-medium">Create new user</h1>
@@ -114,8 +113,9 @@
<div class="flex w-full"> <div class="flex w-full">
<button <button
type="submit" type="submit"
class="m-4 p-2 bg-immich-primary hover:bg-immich-primary/75 px-6 py-4 text-white rounded-md shadow-md w-full" class="m-4 bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
>Create</button >Create
</button
> >
</div> </div>
</form> </form>
@@ -0,0 +1,121 @@
<script lang="ts">
import { api, UserResponseDto } from '@api';
import { createEventDispatcher } from 'svelte';
import AccountEditOutline from 'svelte-material-icons/AccountEditOutline.svelte';
export let user: UserResponseDto;
let error: string;
let success: string;
const dispatch = createEventDispatcher();
// eslint-disable-next-line no-undef
const editUser = async (event: SubmitEvent) => {
try {
const formElement = event.target as HTMLFormElement;
const form = new FormData(formElement);
const firstName = form.get('firstName');
const lastName = form.get('lastName');
const { status } = await api.userApi.updateUser({
id: user.id,
firstName: firstName!.toString(),
lastName: lastName!.toString()
});
if (status == 200) {
dispatch('edit-success');
}
} catch (e) {
console.log('Error updating user ', e);
}
};
const resetPassword = async () => {
try {
if (window.confirm('Do you want to reset the user password?')) {
const defaultPassword = 'password';
const { status } = await api.userApi.updateUser({
id: user.id,
password: defaultPassword,
shouldChangePassword: true
});
if (status == 200) {
dispatch('reset-password-success');
}
}
} catch (e) {
console.log('Error reseting user password', e);
}
};
</script>
<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">
<!-- <img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo"/>-->
<AccountEditOutline size="4em" color="#4250affe" />
<h1 class="text-2xl text-immich-primary font-medium">Edit user</h1>
</div>
<form on:submit|preventDefault={editUser} autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Email (cannot change)</label>
<input
class="immich-form-input disabled:bg-gray-200 hover:cursor-not-allowed"
id="email"
name="email"
type="email"
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">
<label class="immich-form-label" for="lastName">Last Name</label>
<input
class="immich-form-input"
id="lastName"
name="lastName"
type="text"
required
bind:value={user.lastName}
/>
</div>
{#if error}
<p class="text-red-400 ml-4 text-sm">{error}</p>
{/if}
{#if success}
<p class="text-immich-primary ml-4 text-sm">{success}</p>
{/if}
<div class="flex w-full px-4 gap-4 mt-8">
<button
on:click={resetPassword}
class="flex-1 transition-colors bg-[#F9DEDC] hover:bg-red-50 text-[#410E0B] px-6 py-3 rounded-full w-full font-medium"
>Reset password
</button>
<button
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>
+1 -1
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 =
+78 -11
View File
@@ -1,6 +1,7 @@
<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';
export const load: Load = async ({ fetch, session }) => { export const load: Load = async ({ fetch, session }) => {
if (!browser && !session.user) { if (!browser && !session.user) {
@@ -11,10 +12,19 @@
} }
try { try {
const [user, allUsers] = await Promise.all([ const user: UserResponseDto = await fetch('/data/user/get-my-user-info').then((r) =>
fetch('/data/user/get-my-user-info').then((r) => r.json()), r.json()
fetch('/data/user/get-all-users?isAll=false').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'
};
}
return { return {
status: 200, status: 200,
@@ -35,7 +45,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { ImmichUser } from '$lib/models/immich-user';
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';
@@ -43,15 +52,19 @@
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 StatusBox from '$lib/components/shared-components/status-box.svelte'; import StatusBox from '$lib/components/shared-components/status-box.svelte';
import { browser } from '$app/env';
let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT; let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
export let user: ImmichUser; export let user: UserResponseDto;
export let allUsers: UserResponseDto[]; export let allUsers: UserResponseDto[];
let shouldShowCreateUserForm: boolean; let editUser: UserResponseDto;
let shouldShowEditUserForm = false;
let shouldShowCreateUserForm = false;
let shouldShowInfoPanel = false;
const onButtonClicked = (buttonType: CustomEvent) => { const onButtonClicked = (buttonType: CustomEvent) => {
selectedAction = buttonType.detail['actionType'] as AdminSideBarSelection; selectedAction = buttonType.detail['actionType'] as AdminSideBarSelection;
@@ -64,9 +77,27 @@
const onUserCreated = async () => { const onUserCreated = async () => {
const { data } = await api.userApi.getAllUsers(false); const { data } = await api.userApi.getAllUsers(false);
allUsers = data; allUsers = data;
shouldShowCreateUserForm = false; shouldShowCreateUserForm = false;
}; };
const editUserHandler = async (event: CustomEvent) => {
const { user } = event.detail;
editUser = user;
shouldShowEditUserForm = true;
};
const onEditUserSuccess = async () => {
const { data } = await api.userApi.getAllUsers(false);
allUsers = data;
shouldShowEditUserForm = false;
};
const onEditPasswordSuccess = async () => {
const { data } = await api.userApi.getAllUsers(false);
allUsers = data;
shouldShowEditUserForm = false;
shouldShowInfoPanel = true;
};
</script> </script>
<svelte:head> <svelte:head>
@@ -77,8 +108,40 @@
{#if shouldShowCreateUserForm} {#if shouldShowCreateUserForm}
<FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}> <FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
<div>
<CreateUserForm on:user-created={onUserCreated} /> <CreateUserForm on:user-created={onUserCreated} />
</FullScreenModal>
{/if}
{#if shouldShowEditUserForm}
<FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
<EditUserForm
user={editUser}
on:edit-success={onEditUserSuccess}
on:reset-password-success={onEditPasswordSuccess}
/>
</FullScreenModal>
{/if}
{#if shouldShowInfoPanel}
<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>
<p>
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>
<div class="flex w-full">
<button
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> </div>
</FullScreenModal> </FullScreenModal>
{/if} {/if}
@@ -106,7 +169,11 @@
<section id="setting-content" class="relative pt-[85px] flex place-content-center"> <section id="setting-content" class="relative pt-[85px] flex place-content-center">
<section class="w-[800px] pt-4"> <section class="w-[800px] pt-4">
{#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT} {#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
<UserManagement {allUsers} on:createUser={() => (shouldShowCreateUserForm = true)} /> <UserManagement
{allUsers}
on:create-user={() => (shouldShowCreateUserForm = true)}
on:edit-user={editUserHandler}
/>
{/if} {/if}
</section> </section>
</section> </section>
+1 -1
View File
@@ -61,7 +61,7 @@
// Delete album that has no photos and is named 'Untitled' // Delete album that has no photos and is named 'Untitled'
for (const album of albums) { for (const album of albums) {
if (album.albumName === 'Untitled' && album.assets.length === 0) { if (album.albumName === 'Untitled' && album.assetCount === 0) {
const isDeleted = await autoDeleteAlbum(album); const isDeleted = await autoDeleteAlbum(album);
if (isDeleted) { if (isDeleted) {
@@ -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' });
if (res.status == 200 && res.statusText == 'OK') {
goto('/auth/login'); goto('/auth/login');
}
}; };
</script> </script>
+7 -5
View File
@@ -2,6 +2,7 @@
export const prerender = false; export const prerender = false;
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { api } from '@api'; import { api } from '@api';
import { browser } from '$app/env';
export const load: Load = async () => { export const load: Load = async () => {
if (browser) { if (browser) {
@@ -12,14 +13,15 @@
status: 302, status: 302,
redirect: '/photos' redirect: '/photos'
}; };
} catch (e) {} } catch (e) {
}
const {data} = await api.userApi.getUserCount(); const {data} = await api.userApi.getUserCount();
return { return {
status: 200, status: 200,
props: { props: {
isAdminUserExist: data.userCount == 0 ? false : true isAdminUserExist: data.userCount != 0
} }
}; };
} }
@@ -28,12 +30,11 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { browser } from '$app/env';
export let isAdminUserExist: boolean; export let isAdminUserExist: boolean;
async function onGettingStartedClicked() { async function onGettingStartedClicked() {
isAdminUserExist ? goto('/auth/login') : goto('/auth/register'); isAdminUserExist ? await goto('/auth/login') : await goto('/auth/register');
} }
</script> </script>
@@ -50,7 +51,8 @@
<h1 class="text-4xl text-immich-primary font-bold font-immich-title">Welcome to IMMICH Web</h1> <h1 class="text-4xl text-immich-primary font-bold font-immich-title">Welcome to IMMICH Web</h1>
<button <button
class="border px-4 py-2 rounded-md bg-immich-primary hover:bg-immich-primary/75 text-white font-bold w-[200px]" class="border px-4 py-2 rounded-md bg-immich-primary hover:bg-immich-primary/75 text-white font-bold w-[200px]"
on:click={onGettingStartedClicked}>Getting Started</button on:click={onGettingStartedClicked}>Getting Started
</button
> >
</div> </div>
</section> </section>
+1 -1
View File
@@ -254,7 +254,7 @@
> >
<!-- Date group title --> <!-- Date group title -->
<p class="font-medium text-sm text-immich-fg mb-2 flex place-items-center h-6"> <p class="font-medium text-sm text-immich-fg mb-2 flex place-items-center h-6">
{#if (selectedGroupThumbnail === groupIndex && isMouseOverGroup) || isMultiSelectionMode} {#if (selectedGroupThumbnail === groupIndex && isMouseOverGroup) || selectedGroup.has(groupIndex)}
<div <div
in:fly={{ x: -24, duration: 200, opacity: 0.5 }} in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
out:fly={{ x: -24, duration: 200 }} out:fly={{ x: -24, duration: 200 }}
+3 -2
View File
@@ -3,6 +3,7 @@
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { AlbumResponseDto, api, UserResponseDto } from '@api'; import { AlbumResponseDto, api, UserResponseDto } from '@api';
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) {
@@ -40,7 +41,6 @@
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte'; import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte'; import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { browser } from '$app/env';
export let user: UserResponseDto; export let user: UserResponseDto;
export let sharedAlbums: AlbumResponseDto[]; export let sharedAlbums: AlbumResponseDto[];
@@ -98,7 +98,8 @@
<div class="w-full flex flex-col place-items-center"> <div class="w-full flex flex-col place-items-center">
{#each sharedAlbums as album} {#each sharedAlbums as album}
<a sveltekit:prefetch href={`albums/${album.id}`}> <a sveltekit:prefetch href={`albums/${album.id}`}>
<SharedAlbumListTile {album} {user} /></a <SharedAlbumListTile {album} {user}/>
</a
> >
{/each} {/each}
</div> </div>