Compare commits

...

10 Commits

Author SHA1 Message Date
Alex Tran
df0a059a02 Up patch version 2022-11-21 20:26:03 -06:00
Alex
cc697486fc fix(server): Deleted shared users cause problem with album retrival and creation (#1002)
* fix(server): Deleted shared users cause problem with album retrival and creation

* Remove dead code
2022-11-21 20:24:56 -06:00
Alex
2227a6f5f3 Added custom buildscript for XCodeCloud 2022-11-21 13:54:30 -06:00
Alex
a9320f06e8 Added v1.36 release note to website 2022-11-21 12:53:25 -06:00
Alex
39b7ab66d4 chore(mobile): clean up linter problems (#1000) 2022-11-21 06:13:14 -06:00
Alex Tran
bc9ee1d611 Added hotfix release note 2022-11-21 05:41:44 -06:00
Alex
56ce747ffc fix(mobile): freeze on splash screen due to accessing bad state (#998) 2022-11-21 05:29:43 -06:00
Alex
a2f3b2199a fix(server): Admin user not created (#996) 2022-11-20 23:25:03 -06:00
Alex Tran
88b8d34aa6 Update .env.example file 2022-11-20 16:44:33 -06:00
Alex Tran
21fd08e0fb Update Readme 2022-11-20 14:42:09 -06:00
59 changed files with 555 additions and 447 deletions

View File

@@ -72,7 +72,9 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
| Search by metadata, objects and image tags | Yes | No |
| Administrative functions (user management) | N/A | Yes |
| Background backup | Android | N/A |
| Virtual scroll | N/A | Yes |
| Virtual scroll | Yes | Yes |
| OAuth Support | Yes | Yes |
| LivePhotos Backup and Playback (iOS only) | Yes | Yes |
# Support the project

View File

@@ -23,7 +23,9 @@ REDIS_HOSTNAME=immich_redis
# REDIS_SOCKET=
###################################################################################
# Upload File Config
# Upload File Location
#
# This is the location where uploaded files are stored.
###################################################################################
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
@@ -36,19 +38,17 @@ LOG_LEVEL=simple
###################################################################################
# JWT SECRET
###################################################################################
#
# This JWT_SECRET is used to sign the authentication keys for user login
# You should set it to a long randomly generated value
# You can use this command to generate one: openssl rand -base64 128
###################################################################################
JWT_SECRET=
###################################################################################
# Reverse Geocoding
####################################################################################
# DISABLE_REVERSE_GEOCODING=false
#
# Reverse geocoding is done locally which has a small impact on memory usage
# This memory usage can be altered by changing the REVERSE_GEOCODING_PRECISION variable
# This ranges from 0-3 with 3 being the most precise
@@ -56,25 +56,44 @@ JWT_SECRET=
# 2 - Cities > 1000 population: ~150MB RAM
# 1 - Cities > 5000 population: ~80MB RAM
# 0 - Cities > 15000 population: ~40MB RAM
####################################################################################
# DISABLE_REVERSE_GEOCODING=false
# REVERSE_GEOCODING_PRECISION=3
####################################################################################
# WEB - Optional
####################################################################################
#
# Custom message on the login page, should be written in HTML form.
# For example PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
# For example:
# PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
####################################################################################
PUBLIC_LOGIN_PAGE_MESSAGE=
####################################################################################
# Alternative Service Addresses - Optional
####################################################################################
# This is an advanced feature for users who may be running their immich services on different hosts. It will not change which address or port that services bind to within their containers, but it will change where other services look for their peers.
#
# This is an advanced feature for users who may be running their immich services on different hosts.
# It will not change which address or port that services bind to within their containers, but it will change where other services look for their peers.
# Note: immich-microservices is bound to 3002, but no references are made
####################################################################################
# IMMICH_WEB_URL=http://immich-web:3000
# IMMICH_SERVER_URL=http://immich-server:3001
# IMMICH_MACHINE_LEARNING_URL=http://immich-machine-learning:3003
####################################################################################
# OAuth Setting - Optional
#
# These setting will enable OAuth login for your instance of Immich
# Folow the instructions in the page https://immich.app/docs/usage/oauth to set up your OAuth provider
####################################################################################
# OAUTH_ENABLED=false
# OAUTH_ISSUER_URL=
# OAUTH_CLIENT_ID=
# OAUTH_CLIENT_SECRET=
# OAUTH_BUTTON_TEXT=Login with OAuth
# OAUTH_AUTO_REGISTER=true
# OAUTH_SCOPE="openid profile email"

View File

@@ -1,12 +0,0 @@
---
slug: first-blog-post
title: First Blog Post
authors:
name: Gao Wei
title: Docusaurus Core Team
url: https://github.com/wgao19
image_url: https://github.com/wgao19.png
tags: [hola, docusaurus]
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

View File

@@ -1,44 +0,0 @@
---
slug: long-blog-post
title: Long Blog Post
authors: endi
tags: [hello, docusaurus]
---
This is the summary of a very long blog post,
Use a `<!--` `truncate` `-->` comment to limit blog post size in the list view.
<!--truncate-->
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

View File

@@ -1,20 +0,0 @@
---
slug: mdx-blog-post
title: MDX Blog Post
authors: [slorber]
tags: [docusaurus]
---
Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
:::tip
Use the power of React to create interactive blog posts.
```js
<button onClick={() => alert('button clicked!')}>Click me!</button>
```
<button onClick={() => alert('button clicked!')}>Click me!</button>
:::

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -1,25 +0,0 @@
---
slug: welcome
title: Welcome
authors: [slorber, yangshun]
tags: [facebook, hello, docusaurus]
---
[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
Simply add Markdown files (or folders) to the `blog` directory.
Regular blog authors can be added to `authors.yml`.
The blog post date can be extracted from filenames, such as:
- `2019-05-30-welcome.md`
- `2019-05-30-welcome/index.md`
A blog post folder can be convenient to co-locate blog post images:
![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg)
The blog supports tags as well!
**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.

View File

@@ -1,17 +1,5 @@
endi:
name: Endilie Yacop Sucipto
title: Maintainer of Docusaurus
url: https://github.com/endiliey
image_url: https://github.com/endiliey.png
yangshun:
name: Yangshun Tay
title: Front End Engineer @ Facebook
url: https://github.com/yangshun
image_url: https://github.com/yangshun.png
slorber:
name: Sébastien Lorber
title: Docusaurus maintainer
url: https://sebastienlorber.com
image_url: https://github.com/slorber.png
alextran:
name: Alex Tran
title: Maintainer of Immich
url: https://github.com/alextran1502
image_url: https://github.com/alextran1502.png

View File

@@ -0,0 +1,114 @@
---
slug: release-1-36
title: Release v1.36.0
authors: [alextran]
tags: [release]
date: 2022-11-10
---
Hello everyone, it is my pleasure to deliver the new release of Immich to you. The team has been working hard to bring you the new features and improvements. This release includes some big features that the community has been asking since the beginning of Immich. We hope you will enjoy it.
Some notable features are:
- [OAuth integration](#livephoto-ios-support-)
- [LivePhoto support on iOS](#oauth-integration-)
- User config system
<!--truncate-->
## LivePhoto iOS Support 🎉
LivePhoto on iOS is now supported in Immich.
The motion part will now be uploaded and can be played on the mobile app and the web.
:::caution
- The server and the app has to be on version **1.36.x** for the application to work correctly.
- Previous uploaded photos will not be updated automatically, you will have to remove and reupload them if you want to keep the LivePhoto functionality.
:::
<img
src="https://media.giphy.com/media/fTrGceZd7t1ewi8ESc/giphy.gif"
width="100%"
style={{
borderRadius: "10px",
boxShadow:
"rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px",
}}
title="LivePhoto playback on the web"
/>
## OAuth Integration 🎉
I want to borrow this chance to express my gratitude to [@EnricoBilla](https://github.com/EnricoBilla), who has been the trailblazer for this feature since the beginning days of Immich. His PR has sparked ideas, suggestions, and discussion among the team member on how to integrate this feature successfully into the app. Thank you so much for your work and your time.
OAuth is now integrated into the system. Please follow the guide [here](https://immich.app/docs/usage/oauth) to set up your OAuth integration
After setting up the correct environment variables in the `.env` file, as shown below
| Key | Type | Default | Description |
| ------------------- | ------- | -------------------- | ------------------------------------------------------------------------- |
| OAUTH_ENABLED | boolean | false | Enable/disable OAuth2 |
| OAUTH_ISSUER_URL | URL | (required) | Required. Self-discovery URL for client |
| OAUTH_CLIENT_ID | string | (required) | Required. Client ID |
| OAUTH_CLIENT_SECRET | string | (required) | Required. Client Secret |
| OAUTH_SCOPE | string | openid email profile | Full list of scopes to send with the request (space delimited) |
| OAUTH_AUTO_REGISTER | boolean | true | When true, will automatically register a user the first time they sign in |
| OAUTH_BUTTON_TEXT | string | Login with OAuth | Text for the OAuth button on the web |
```bash title="Authentik Example"
OAUTH_ENABLED=true
OAUTH_ISSUER_URL=http://10.1.15.216:9000/application/o/immich-test/
OAUTH_CLIENT_ID=30596v8f78a4b6a97d5985c3076b6b4c4d12ddc33
OAUTH_CLIENT_SECRET=50f1eafdec353b95b1c638db390db4ab67ef035a51212dbec2f56175e2eb272b5d572c099176e6fe116ecf47ffdd544bgdb9e2edc588307ee0339d25eeccd88
OAUTH_BUTTON_TEXT=Login with Authentik
```
The web will have the option to sign in with OAuth.
<img
src="https://user-images.githubusercontent.com/27055614/202923726-f43fa148-47f5-4182-8f29-b0b87e4586fa.png"
width="50%"
title="Web Sign in with OAuth"
style={{
borderRadius: "10px",
boxShadow:
"rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px",
}}
/>
The mobile app will check if the server has OAuth enabled before displaying the OAuth
sign-in button.
<img
src="https://media.giphy.com/media/3iy3SaNkVYtlkEiw06/giphy.gif"
title="Mobile sign in with OAuth"
style={{
borderRadius: "10px",
boxShadow:
"rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px",
}}
/>
## Support
<img
src="https://media.giphy.com/media/LStqgGESXW8XnuCv5y/giphy.gif"
width="300"
style={{
borderRadius: "10px",
boxShadow:
"rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px",
}}
title="Support the project"
/>
If you find the project helpful and it helps you in some ways, you can support the project [one time](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) or [monthly](https://github.com/sponsors/alextran1502) from GitHub Sponsor
It is a great way to let me know that you want me to continue developing and working on this project for years to come.
## Details
For more details, please check out the [release note](https://github.com/immich-app/immich/releases/tag/v1.36.0_55-dev)

View File

@@ -49,7 +49,7 @@ Once you have a new OAuth client application configured, Immich can be configure
| OAUTH_ENABLED | boolean | false | Enable/disable OAuth2 |
| OAUTH_ISSUER_URL | URL | (required) | Required. Self-discovery URL for client (from previous step) |
| OAUTH_CLIENT_ID | string | (required) | Required. Client ID (from previous step) |
| OAUTH_CLIENT_SECRET | string | (required) | Required. Client Secret (previous step |
| OAUTH_CLIENT_SECRET | string | (required) | Required. Client Secret (previous step) |
| OAUTH_SCOPE | string | openid email profile | Full list of scopes to send with the request (space delimited) |
| OAUTH_AUTO_REGISTER | boolean | true | When true, will automatically register a user the first time they sign in |
| OAUTH_BUTTON_TEXT | string | Login with OAuth | Text for the OAuth button on the web |

View File

@@ -80,7 +80,7 @@ const config = {
position: "right",
label: "Documentation",
},
// { to: "/blog", label: "Blog", position: "right" },
{ to: "/blog", label: "Blog", position: "right" },
{
href: "https://github.com/immich-app/immich",
label: "GitHub",

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 55,
"android.injected.version.name" => "1.36.0",
"android.injected.version.code" => 56,
"android.injected.version.name" => "1.36.1",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -0,0 +1,2 @@
* Fixed freezed splash screen
* Fixed OIDC redirect but not logging in

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000315">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000345">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="185.624188">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="123.14891">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="39.180655">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="39.270764">
</testcase>

View File

@@ -360,7 +360,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 71;
CURRENT_PROJECT_VERSION = 72;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -495,7 +495,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 71;
CURRENT_PROJECT_VERSION = 72;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -522,7 +522,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 71;
CURRENT_PROJECT_VERSION = 72;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.36.0</string>
<string>1.36.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>71</string>
<string>72</string>
<key>LSRequiresIPhoneOS</key>
<true />
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>

View File

@@ -0,0 +1,23 @@
#!/bin/sh
# The default execution directory of this script is the ci_scripts directory.
cd $CI_WORKSPACE/mobile
# Install Flutter using git.
git clone https://github.com/flutter/flutter.git --depth 1 -b stable $HOME/flutter
export PATH="$PATH:$HOME/flutter/bin"
# Install Flutter artifacts for iOS (--ios), or macOS (--macos) platforms.
flutter precache --ios
# Install Flutter dependencies.
flutter pub get
# Install CocoaPods using Homebrew.
HOMEBREW_NO_AUTO_UPDATE=1 # disable homebrew's automatic updates.
brew install cocoapods
# Install CocoaPods dependencies.
cd ios && pod install # run `pod install` in the `ios` directory.
exit 0

View File

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

View File

@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000333">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000358">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.777934">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.721922">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="6.375897">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="6.015111">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.664307">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.656945">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="88.90147">
<testcase classname="fastlane.lanes" name="4: build_app" time="75.686541">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="79.807067">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="68.644406">
</testcase>

View File

@@ -69,7 +69,10 @@ class AlbumService {
Iterable<Asset> assets,
) async {
return createAlbum(
_getNextAlbumName(await getAlbums(isShared: false)), assets, []);
_getNextAlbumName(await getAlbums(isShared: false)),
assets,
[],
);
}
Future<AlbumResponseDto?> getAlbumDetail(String albumId) async {

View File

@@ -34,7 +34,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
void _onDeleteAlbumPressed(String albumId) async {
void onDeleteAlbumPressed(String albumId) async {
ImmichLoadingOverlayController.appLoader.show();
bool isSuccess =
@@ -62,7 +62,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
ImmichLoadingOverlayController.appLoader.hide();
}
void _onLeaveAlbumPressed(String albumId) async {
void onLeaveAlbumPressed(String albumId) async {
ImmichLoadingOverlayController.appLoader.show();
bool isSuccess =
@@ -84,7 +84,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
ImmichLoadingOverlayController.appLoader.hide();
}
void _onRemoveFromAlbumPressed(String albumId) async {
void onRemoveFromAlbumPressed(String albumId) async {
ImmichLoadingOverlayController.appLoader.show();
bool isSuccess =
@@ -110,7 +110,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
ImmichLoadingOverlayController.appLoader.hide();
}
_buildBottomSheetActionButton() {
buildBottomSheetActionButton() {
if (isMultiSelectionEnable) {
if (albumInfo.ownerId == userId) {
return ListTile(
@@ -119,7 +119,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
'album_viewer_appbar_share_remove',
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
onTap: () => _onRemoveFromAlbumPressed(albumId),
onTap: () => onRemoveFromAlbumPressed(albumId),
);
} else {
return const SizedBox();
@@ -132,7 +132,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
'album_viewer_appbar_share_delete',
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
onTap: () => _onDeleteAlbumPressed(albumId),
onTap: () => onDeleteAlbumPressed(albumId),
);
} else {
return ListTile(
@@ -141,13 +141,13 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
'album_viewer_appbar_share_leave',
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
onTap: () => _onLeaveAlbumPressed(albumId),
onTap: () => onLeaveAlbumPressed(albumId),
);
}
}
}
void _buildBottomSheet() {
void buildBottomSheet() {
showModalBottomSheet(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: false,
@@ -157,7 +157,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildBottomSheetActionButton(),
buildBottomSheetActionButton(),
],
),
);
@@ -165,7 +165,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
);
}
_buildLeadingButton() {
buildLeadingButton() {
if (isMultiSelectionEnable) {
return IconButton(
onPressed: () => ref
@@ -204,7 +204,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
return AppBar(
elevation: 0,
leading: _buildLeadingButton(),
leading: buildLeadingButton(),
title: isMultiSelectionEnable
? Text('${selectedAssetsInAlbum.length}')
: null,
@@ -212,7 +212,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
actions: [
IconButton(
splashRadius: 25,
onPressed: _buildBottomSheet,
onPressed: buildBottomSheet,
icon: const Icon(Icons.more_horiz_rounded),
),
],

View File

@@ -27,7 +27,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
final isMultiSelectionEnable =
ref.watch(assetSelectionProvider).isMultiselectEnable;
_viewAsset() {
viewAsset() {
AutoRouter.of(context).push(
GalleryViewerRoute(
asset: asset,
@@ -47,18 +47,18 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
}
}
_enableMultiSelection() {
enableMultiSelection() {
ref.watch(assetSelectionProvider.notifier).enableMultiselection();
ref
.watch(assetSelectionProvider.notifier)
.addAssetsInAlbumViewer([asset]);
}
_disableMultiSelection() {
disableMultiSelection() {
ref.watch(assetSelectionProvider.notifier).disableMultiselection();
}
_buildVideoLabel() {
buildVideoLabel() {
return Positioned(
top: 5,
right: 5,
@@ -80,7 +80,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
);
}
_buildAssetStoreLocationIcon() {
buildAssetStoreLocationIcon() {
return Positioned(
right: 10,
bottom: 5,
@@ -94,7 +94,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
);
}
_buildAssetSelectionIcon() {
buildAssetSelectionIcon() {
bool isSelected = selectedAssetsInAlbumViewer.contains(asset);
return Positioned(
@@ -112,21 +112,21 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
);
}
_buildThumbnailImage() {
buildThumbnailImage() {
return Container(
decoration: BoxDecoration(border: drawBorderColor()),
child: ImmichImage(asset, width: 300, height: 300),
);
}
_handleSelectionGesture() {
handleSelectionGesture() {
if (selectedAssetsInAlbumViewer.contains(asset)) {
ref
.watch(assetSelectionProvider.notifier)
.removeAssetsInAlbumViewer([asset]);
if (selectedAssetsInAlbumViewer.isEmpty) {
_disableMultiSelection();
disableMultiSelection();
}
} else {
ref
@@ -136,14 +136,14 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
}
return GestureDetector(
onTap: isMultiSelectionEnable ? _handleSelectionGesture : _viewAsset,
onLongPress: _enableMultiSelection,
onTap: isMultiSelectionEnable ? handleSelectionGesture : viewAsset,
onLongPress: enableMultiSelection,
child: Stack(
children: [
_buildThumbnailImage(),
if (showStorageIndicator) _buildAssetStoreLocationIcon(),
if (!asset.isImage) _buildVideoLabel(),
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
buildThumbnailImage(),
if (showStorageIndicator) buildAssetStoreLocationIcon(),
if (!asset.isImage) buildVideoLabel(),
if (isMultiSelectionEnable) buildAssetSelectionIcon(),
],
),
);

View File

@@ -21,7 +21,7 @@ class MonthGroupTitle extends HookConsumerWidget {
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
_handleTitleIconClick() {
handleTitleIconClick() {
HapticFeedback.heavyImpact();
if (isAlbumExist) {
@@ -61,7 +61,7 @@ class MonthGroupTitle extends HookConsumerWidget {
}
}
_getSimplifiedMonth() {
getSimplifiedMonth() {
var monthAndYear = month.split(',');
var yearText = monthAndYear[1].trim();
var monthText = monthAndYear[0].trim();
@@ -85,7 +85,7 @@ class MonthGroupTitle extends HookConsumerWidget {
child: Row(
children: [
GestureDetector(
onTap: _handleTitleIconClick,
onTap: handleTitleIconClick,
child: selectedDateGroup.contains(month)
? Icon(
Icons.check_circle_rounded,
@@ -97,11 +97,11 @@ class MonthGroupTitle extends HookConsumerWidget {
),
),
GestureDetector(
onTap: _handleTitleIconClick,
onTap: handleTitleIconClick,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
_getSimplifiedMonth(),
getSimplifiedMonth(),
style: TextStyle(
fontSize: 24,
color: Theme.of(context).primaryColor,

View File

@@ -18,7 +18,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
Widget _buildSelectionIcon(Asset asset) {
Widget buildSelectionIcon(Asset asset) {
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
var isNewlySelected =
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
@@ -111,7 +111,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
padding: const EdgeInsets.all(3.0),
child: Align(
alignment: Alignment.topLeft,
child: _buildSelectionIcon(asset),
child: buildSelectionIcon(asset),
),
),
if (!asset.isImage)

View File

@@ -37,7 +37,7 @@ class AlbumViewerPage extends HookConsumerWidget {
/// Find out if the assets in album exist on the device
/// If they exist, add to selected asset state to show they are already selected.
void _onAddPhotosPressed(AlbumResponseDto albumInfo) async {
void onAddPhotosPressed(AlbumResponseDto albumInfo) async {
if (albumInfo.assets.isNotEmpty == true) {
ref.watch(assetSelectionProvider.notifier).addNewAssets(
albumInfo.assets.map((e) => Asset.remote(e)).toList(),
@@ -60,7 +60,8 @@ class AlbumViewerPage extends HookConsumerWidget {
albumId,
);
if (addAssetsResult != null && addAssetsResult.successfullyAdded > 0) {
if (addAssetsResult != null &&
addAssetsResult.successfullyAdded > 0) {
ref.refresh(sharedAlbumDetailProvider(albumId));
}
@@ -73,7 +74,7 @@ class AlbumViewerPage extends HookConsumerWidget {
}
}
void _onAddUsersPressed(AlbumResponseDto albumInfo) async {
void onAddUsersPressed(AlbumResponseDto albumInfo) async {
List<String>? sharedUserIds =
await AutoRouter.of(context).push<List<String>?>(
SelectAdditionalUserForSharingRoute(albumInfo: albumInfo),
@@ -94,7 +95,7 @@ class AlbumViewerPage extends HookConsumerWidget {
}
}
Widget _buildTitle(AlbumResponseDto albumInfo) {
Widget buildTitle(AlbumResponseDto albumInfo) {
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
child: userId == albumInfo.ownerId
@@ -115,7 +116,7 @@ class AlbumViewerPage extends HookConsumerWidget {
);
}
Widget _buildAlbumDateRange(AlbumResponseDto albumInfo) {
Widget buildAlbumDateRange(AlbumResponseDto albumInfo) {
String startDate = "";
DateTime parsedStartDate =
DateTime.parse(albumInfo.assets.first.createdAt);
@@ -148,14 +149,14 @@ class AlbumViewerPage extends HookConsumerWidget {
);
}
Widget _buildHeader(AlbumResponseDto albumInfo) {
Widget buildHeader(AlbumResponseDto albumInfo) {
return SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(albumInfo),
buildTitle(albumInfo),
if (albumInfo.assets.isNotEmpty == true)
_buildAlbumDateRange(albumInfo),
buildAlbumDateRange(albumInfo),
if (albumInfo.shared)
SizedBox(
height: 60,
@@ -188,7 +189,7 @@ class AlbumViewerPage extends HookConsumerWidget {
);
}
Widget _buildImageGrid(AlbumResponseDto albumInfo) {
Widget buildImageGrid(AlbumResponseDto albumInfo) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final bool showStorageIndicator =
appSettingService.getSetting(AppSettingsEnum.storageIndicator);
@@ -220,7 +221,7 @@ class AlbumViewerPage extends HookConsumerWidget {
return const SliverToBoxAdapter();
}
Widget _buildControlButton(AlbumResponseDto albumInfo) {
Widget buildControlButton(AlbumResponseDto albumInfo) {
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
child: SizedBox(
@@ -230,13 +231,13 @@ class AlbumViewerPage extends HookConsumerWidget {
children: [
AlbumActionOutlinedButton(
iconData: Icons.add_photo_alternate_outlined,
onPressed: () => _onAddPhotosPressed(albumInfo),
onPressed: () => onAddPhotosPressed(albumInfo),
labelText: "share_add_photos".tr(),
),
if (userId == albumInfo.ownerId)
AlbumActionOutlinedButton(
iconData: Icons.person_add_alt_rounded,
onPressed: () => _onAddUsersPressed(albumInfo),
onPressed: () => onAddUsersPressed(albumInfo),
labelText: "album_viewer_page_share_add_users".tr(),
),
],
@@ -245,7 +246,7 @@ class AlbumViewerPage extends HookConsumerWidget {
);
}
Widget _buildBody(AlbumResponseDto albumInfo) {
Widget buildBody(AlbumResponseDto albumInfo) {
return GestureDetector(
onTap: () {
titleFocusNode.unfocus();
@@ -257,7 +258,7 @@ class AlbumViewerPage extends HookConsumerWidget {
child: CustomScrollView(
controller: scrollController,
slivers: [
_buildHeader(albumInfo),
buildHeader(albumInfo),
SliverPersistentHeader(
pinned: true,
delegate: ImmichSliverPersistentAppBarDelegate(
@@ -265,11 +266,11 @@ class AlbumViewerPage extends HookConsumerWidget {
maxHeight: 50,
child: Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: _buildControlButton(albumInfo),
child: buildControlButton(albumInfo),
),
),
),
_buildImageGrid(albumInfo)
buildImageGrid(albumInfo)
],
),
),
@@ -293,7 +294,7 @@ class AlbumViewerPage extends HookConsumerWidget {
),
body: albumInfo.when(
data: (albumInfo) => albumInfo != null
? _buildBody(albumInfo)
? buildBody(albumInfo)
: const Center(
child: CircularProgressIndicator(),
),

View File

@@ -25,7 +25,7 @@ class AssetSelectionPage extends HookConsumerWidget {
List<Widget> imageGridGroup = [];
String _buildAssetCountText() {
String buildAssetCountText() {
if (isAlbumExist) {
return (selectedAssets.length + newAssetsForAlbum.length).toString();
} else {
@@ -33,7 +33,7 @@ class AssetSelectionPage extends HookConsumerWidget {
}
}
Widget _buildBody() {
Widget buildBody() {
assetGroupMonthYear.forEach((monthYear, assetGroup) {
imageGridGroup
.add(MonthGroupTitle(month: monthYear, assetGroup: assetGroup));
@@ -71,7 +71,7 @@ class AssetSelectionPage extends HookConsumerWidget {
style: TextStyle(fontSize: 18),
).tr()
: Text(
_buildAssetCountText(),
buildAssetCountText(),
style: const TextStyle(fontSize: 18),
),
centerTitle: false,
@@ -94,7 +94,7 @@ class AssetSelectionPage extends HookConsumerWidget {
),
],
),
body: _buildBody(),
body: buildBody(),
);
}
}

View File

@@ -29,11 +29,11 @@ class CreateAlbumPage extends HookConsumerWidget {
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
_showSelectUserPage() {
showSelectUserPage() {
AutoRouter.of(context).push(const SelectUserForSharingRoute());
}
void _onBackgroundTapped() {
void onBackgroundTapped() {
albumTitleTextFieldFocusNode.unfocus();
isAlbumTitleTextFieldFocus.value = false;
@@ -45,7 +45,7 @@ class CreateAlbumPage extends HookConsumerWidget {
}
}
_onSelectPhotosButtonPressed() async {
onSelectPhotosButtonPressed() async {
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(false);
AssetSelectionPageResult? selectedAsset = await AutoRouter.of(context)
@@ -56,7 +56,7 @@ class CreateAlbumPage extends HookConsumerWidget {
}
}
_buildTitleInputField() {
buildTitleInputField() {
return Padding(
padding: const EdgeInsets.only(
right: 10,
@@ -71,7 +71,7 @@ class CreateAlbumPage extends HookConsumerWidget {
);
}
_buildTitle() {
buildTitle() {
if (selectedAssets.isEmpty) {
return SliverToBoxAdapter(
child: Padding(
@@ -90,7 +90,7 @@ class CreateAlbumPage extends HookConsumerWidget {
return const SliverToBoxAdapter();
}
_buildSelectPhotosButton() {
buildSelectPhotosButton() {
if (selectedAssets.isEmpty) {
return SliverToBoxAdapter(
child: Padding(
@@ -109,7 +109,7 @@ class CreateAlbumPage extends HookConsumerWidget {
borderRadius: BorderRadius.circular(5),
),
),
onPressed: _onSelectPhotosButtonPressed,
onPressed: onSelectPhotosButtonPressed,
icon: Icon(
Icons.add_rounded,
color: Theme.of(context).primaryColor,
@@ -132,7 +132,7 @@ class CreateAlbumPage extends HookConsumerWidget {
return const SliverToBoxAdapter();
}
_buildControlButton() {
buildControlButton() {
return Padding(
padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16),
child: SizedBox(
@@ -142,7 +142,7 @@ class CreateAlbumPage extends HookConsumerWidget {
children: [
AlbumActionOutlinedButton(
iconData: Icons.add_photo_alternate_outlined,
onPressed: _onSelectPhotosButtonPressed,
onPressed: onSelectPhotosButtonPressed,
labelText: "share_add_photos".tr(),
),
],
@@ -151,7 +151,7 @@ class CreateAlbumPage extends HookConsumerWidget {
);
}
_buildSelectedImageGrid() {
buildSelectedImageGrid() {
if (selectedAssets.isNotEmpty) {
return SliverPadding(
padding: const EdgeInsets.only(top: 16),
@@ -164,7 +164,7 @@ class CreateAlbumPage extends HookConsumerWidget {
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return GestureDetector(
onTap: _onBackgroundTapped,
onTap: onBackgroundTapped,
child: SharedAlbumThumbnailImage(
asset: selectedAssets.elementAt(index),
),
@@ -179,7 +179,7 @@ class CreateAlbumPage extends HookConsumerWidget {
return const SliverToBoxAdapter();
}
_createNonSharedAlbum() async {
createNonSharedAlbum() async {
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
ref.watch(albumTitleProvider),
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
@@ -216,7 +216,7 @@ class CreateAlbumPage extends HookConsumerWidget {
if (isSharedAlbum)
TextButton(
onPressed: albumTitleController.text.isNotEmpty
? _showSelectUserPage
? showSelectUserPage
: null,
child: Text(
'create_shared_album_page_share'.tr(),
@@ -230,7 +230,7 @@ class CreateAlbumPage extends HookConsumerWidget {
TextButton(
onPressed: albumTitleController.text.isNotEmpty &&
selectedAssets.isNotEmpty
? _createNonSharedAlbum
? createNonSharedAlbum
: null,
child: Text(
'create_shared_album_page_create'.tr(),
@@ -242,7 +242,7 @@ class CreateAlbumPage extends HookConsumerWidget {
],
),
body: GestureDetector(
onTap: _onBackgroundTapped,
onTap: onBackgroundTapped,
child: CustomScrollView(
slivers: [
SliverAppBar(
@@ -255,15 +255,15 @@ class CreateAlbumPage extends HookConsumerWidget {
preferredSize: const Size.fromHeight(66.0),
child: Column(
children: [
_buildTitleInputField(),
if (selectedAssets.isNotEmpty) _buildControlButton(),
buildTitleInputField(),
if (selectedAssets.isNotEmpty) buildControlButton(),
],
),
),
),
_buildTitle(),
_buildSelectPhotosButton(),
_buildSelectedImageGrid(),
buildTitle(),
buildSelectPhotosButton(),
buildSelectedImageGrid(),
],
),
),

View File

@@ -19,12 +19,12 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
ref.watch(suggestedSharedUsersProvider);
final sharedUsersList = useState<Set<UserResponseDto>>({});
_addNewUsersHandler() {
addNewUsersHandler() {
AutoRouter.of(context)
.pop(sharedUsersList.value.map((e) => e.id).toList());
}
_buildTileIcon(UserResponseDto user) {
buildTileIcon(UserResponseDto user) {
if (sharedUsersList.value.contains(user)) {
return CircleAvatar(
backgroundColor: Theme.of(context).primaryColor,
@@ -42,7 +42,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
}
}
_buildUserList(List<UserResponseDto> users) {
buildUserList(List<UserResponseDto> users) {
List<Widget> usersChip = [];
for (var user in sharedUsersList.value) {
@@ -84,7 +84,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
shrinkWrap: true,
itemBuilder: ((context, index) {
return ListTile(
leading: _buildTileIcon(users[index]),
leading: buildTileIcon(users[index]),
title: Text(
users[index].email,
style: const TextStyle(
@@ -131,7 +131,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
actions: [
TextButton(
onPressed:
sharedUsersList.value.isEmpty ? null : _addNewUsersHandler,
sharedUsersList.value.isEmpty ? null : addNewUsersHandler,
child: const Text(
"share_add",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
@@ -147,7 +147,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
);
}
return _buildUserList(users);
return buildUserList(users);
},
error: (e, _) => Text("Error loading suggested users $e"),
loading: () => const Center(

View File

@@ -20,7 +20,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
AsyncValue<List<UserResponseDto>> suggestedShareUsers =
ref.watch(suggestedSharedUsersProvider);
_createSharedAlbum() async {
createSharedAlbum() async {
var newAlbum =
await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
ref.watch(albumTitleProvider),
@@ -44,7 +44,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
);
}
_buildTileIcon(UserResponseDto user) {
buildTileIcon(UserResponseDto user) {
if (sharedUsersList.value.contains(user)) {
return CircleAvatar(
backgroundColor: Theme.of(context).primaryColor,
@@ -62,7 +62,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
}
}
_buildUserList(List<UserResponseDto> users) {
buildUserList(List<UserResponseDto> users) {
List<Widget> usersChip = [];
for (var user in sharedUsersList.value) {
@@ -104,7 +104,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
shrinkWrap: true,
itemBuilder: ((context, index) {
return ListTile(
leading: _buildTileIcon(users[index]),
leading: buildTileIcon(users[index]),
title: Text(
users[index].email,
style: const TextStyle(
@@ -153,8 +153,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
),
onPressed:
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum,
child: const Text(
"share_create_album",
style: TextStyle(
@@ -168,7 +167,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
),
body: suggestedShareUsers.when(
data: (users) {
return _buildUserList(users);
return buildUserList(users);
},
error: (e, _) => Text("Error loading suggested users $e"),
loading: () => const Center(

View File

@@ -28,7 +28,7 @@ class SharingPage extends HookConsumerWidget {
[],
);
_buildAlbumList() {
buildAlbumList() {
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
@@ -71,7 +71,7 @@ class SharingPage extends HookConsumerWidget {
);
}
_buildEmptyListIndication() {
buildEmptyListIndication() {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
@@ -136,8 +136,8 @@ class SharingPage extends HookConsumerWidget {
),
),
sharedAlbums.isNotEmpty
? _buildAlbumList()
: _buildEmptyListIndication()
? buildAlbumList()
: buildEmptyListIndication()
],
),
);

View File

@@ -64,5 +64,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
final imageViewerStateProvider =
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
((ref) => ImageViewerStateNotifier(
ref.watch(imageViewerServiceProvider), ref.watch(shareServiceProvider))),
ref.watch(imageViewerServiceProvider),
ref.watch(shareServiceProvider),
)),
);

View File

@@ -15,7 +15,7 @@ class ExifBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
_buildMap() {
buildMap() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Container(
@@ -66,7 +66,7 @@ class ExifBottomSheet extends ConsumerWidget {
ExifResponseDto? exifInfo = assetDetail.remote?.exifInfo;
_buildLocationText() {
buildLocationText() {
return Text(
"${exifInfo?.city}, ${exifInfo?.state}",
style: TextStyle(
@@ -120,11 +120,11 @@ class ExifBottomSheet extends ConsumerWidget {
).tr(),
if (assetDetail.latitude != null &&
assetDetail.longitude != null)
_buildMap(),
buildMap(),
if (exifInfo != null &&
exifInfo.city != null &&
exifInfo.state != null)
_buildLocationText(),
buildLocationText(),
Text(
"${assetDetail.latitude?.toStringAsFixed(4)}, ${assetDetail.longitude?.toStringAsFixed(4)}",
style: TextStyle(fontSize: 12, color: Colors.grey[400]),

View File

@@ -1,3 +1,5 @@
// ignore_for_file: implementation_imports
import 'package:flutter/foundation.dart';
import 'package:easy_localization/src/asset_loader.dart';
import 'package:easy_localization/src/easy_localization_controller.dart';

View File

@@ -33,7 +33,7 @@ class AlbumInfoCard extends HookConsumerWidget {
ColorFilter unselectedFilter =
const ColorFilter.mode(Colors.black, BlendMode.color);
_buildSelectedTextBox() {
buildSelectedTextBox() {
if (isSelected) {
return Chip(
visualDensity: VisualDensity.compact,
@@ -67,7 +67,7 @@ class AlbumInfoCard extends HookConsumerWidget {
return const SizedBox();
}
_buildImageFilter() {
buildImageFilter() {
if (isSelected) {
return selectedFilter;
} else if (isExcluded) {
@@ -163,7 +163,7 @@ class AlbumInfoCard extends HookConsumerWidget {
topRight: Radius.circular(12),
),
image: DecorationImage(
colorFilter: _buildImageFilter(),
colorFilter: buildImageFilter(),
image: imageData != null
? MemoryImage(imageData!)
: const AssetImage(
@@ -177,7 +177,7 @@ class AlbumInfoCard extends HookConsumerWidget {
Positioned(
bottom: 10,
left: 25,
child: _buildSelectedTextBox(),
child: buildSelectedTextBox(),
)
],
),

View File

@@ -15,14 +15,16 @@ class AlbumPreviewPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final assets = useState<List<AssetEntity>>([]);
_getAssetsInAlbum() async {
getAssetsInAlbum() async {
assets.value = await album.getAssetListRange(
start: 0, end: await album.assetCountAsync);
start: 0,
end: await album.assetCountAsync,
);
}
useEffect(
() {
_getAssetsInAlbum();
getAssetsInAlbum();
return null;
},
[],

View File

@@ -27,7 +27,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
[],
);
_buildAlbumSelectionList() {
buildAlbumSelectionList() {
if (availableAlbums.isEmpty) {
return const Center(
child: ImmichLoadingIndicator(),
@@ -56,7 +56,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
);
}
_buildSelectedAlbumNameChip() {
buildSelectedAlbumNameChip() {
return selectedBackupAlbums.map((album) {
void removeSelection() {
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
@@ -104,7 +104,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
}).toSet();
}
_buildExcludedAlbumNameChip() {
buildExcludedAlbumNameChip() {
return excludedBackupAlbums.map((album) {
void removeSelection() {
ref
@@ -177,8 +177,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap(
children: [
..._buildSelectedAlbumNameChip(),
..._buildExcludedAlbumNameChip()
...buildSelectedAlbumNameChip(),
...buildExcludedAlbumNameChip()
],
),
),
@@ -286,7 +286,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildAlbumSelectionList(),
child: buildAlbumSelectionList(),
),
],
),

View File

@@ -45,7 +45,7 @@ class BackupControllerPage extends HookConsumerWidget {
[],
);
Widget _buildStorageInformation() {
Widget buildStorageInformation() {
return ListTile(
leading: Icon(
Icons.storage_rounded,
@@ -84,7 +84,7 @@ class BackupControllerPage extends HookConsumerWidget {
);
}
ListTile _buildAutoBackupController() {
ListTile buildAutoBackupController() {
var backUpOption = authenticationState.deviceInfo.isAutoBackup
? "backup_controller_page_status_on".tr()
: "backup_controller_page_status_off".tr();
@@ -143,7 +143,7 @@ class BackupControllerPage extends HookConsumerWidget {
);
}
void _showErrorToUser(String msg) {
void showErrorToUser(String msg) {
final snackBar = SnackBar(
content: Text(
msg.tr(),
@@ -153,7 +153,7 @@ class BackupControllerPage extends HookConsumerWidget {
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
void _showBatteryOptimizationInfoToUser() {
void showBatteryOptimizationInfoToUser() {
showDialog<void>(
context: context,
barrierDismissible: false,
@@ -193,7 +193,7 @@ class BackupControllerPage extends HookConsumerWidget {
);
}
ListTile _buildBackgroundBackupController() {
ListTile buildBackgroundBackupController() {
final bool isBackgroundEnabled = backupState.backgroundBackup;
final bool isWifiRequired = backupState.backupRequireWifi;
final bool isChargingRequired = backupState.backupRequireCharging;
@@ -238,8 +238,8 @@ class BackupControllerPage extends HookConsumerWidget {
.read(backupProvider.notifier)
.configureBackgroundBackup(
requireWifi: isChecked,
onError: _showErrorToUser,
onBatteryInfo: _showBatteryOptimizationInfoToUser,
onError: showErrorToUser,
onBatteryInfo: showBatteryOptimizationInfoToUser,
)
: null,
),
@@ -259,8 +259,8 @@ class BackupControllerPage extends HookConsumerWidget {
.read(backupProvider.notifier)
.configureBackgroundBackup(
requireCharging: isChecked,
onError: _showErrorToUser,
onBatteryInfo: _showBatteryOptimizationInfoToUser,
onError: showErrorToUser,
onBatteryInfo: showBatteryOptimizationInfoToUser,
)
: null,
),
@@ -268,8 +268,8 @@ class BackupControllerPage extends HookConsumerWidget {
onPressed: () =>
ref.read(backupProvider.notifier).configureBackgroundBackup(
enabled: !isBackgroundEnabled,
onError: _showErrorToUser,
onBatteryInfo: _showBatteryOptimizationInfoToUser,
onError: showErrorToUser,
onBatteryInfo: showBatteryOptimizationInfoToUser,
),
child: Text(
isBackgroundEnabled
@@ -284,7 +284,7 @@ class BackupControllerPage extends HookConsumerWidget {
);
}
Widget _buildSelectedAlbumName() {
Widget buildSelectedAlbumName() {
var text = "backup_controller_page_backup_selected".tr();
var albums = ref.watch(backupProvider).selectedBackupAlbums;
@@ -323,7 +323,7 @@ class BackupControllerPage extends HookConsumerWidget {
}
}
Widget _buildExcludedAlbumName() {
Widget buildExcludedAlbumName() {
var text = "backup_controller_page_excluded".tr();
var albums = ref.watch(backupProvider).excludedBackupAlbums;
@@ -348,7 +348,7 @@ class BackupControllerPage extends HookConsumerWidget {
}
}
_buildFolderSelectionTile() {
buildFolderSelectionTile() {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), // if you need this
@@ -374,8 +374,8 @@ class BackupControllerPage extends HookConsumerWidget {
"backup_controller_page_to_backup",
style: TextStyle(fontSize: 12),
).tr(),
_buildSelectedAlbumName(),
_buildExcludedAlbumName()
buildSelectedAlbumName(),
buildExcludedAlbumName()
],
),
),
@@ -398,7 +398,7 @@ class BackupControllerPage extends HookConsumerWidget {
);
}
_buildCurrentBackupAssetInfoCard() {
buildCurrentBackupAssetInfoCard() {
return ListTile(
leading: Icon(
Icons.info_outline_rounded,
@@ -606,7 +606,7 @@ class BackupControllerPage extends HookConsumerWidget {
),
),
),
_buildFolderSelectionTile(),
buildFolderSelectionTile(),
BackupInfoCard(
title: "backup_controller_page_total".tr(),
subtitle: "backup_controller_page_total_sub".tr(),
@@ -624,13 +624,13 @@ class BackupControllerPage extends HookConsumerWidget {
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
),
const Divider(),
_buildAutoBackupController(),
buildAutoBackupController(),
if (Platform.isAndroid) const Divider(),
if (Platform.isAndroid) _buildBackgroundBackupController(),
if (Platform.isAndroid) buildBackgroundBackupController(),
const Divider(),
_buildStorageInformation(),
buildStorageInformation(),
const Divider(),
_buildCurrentBackupAssetInfoCard(),
buildCurrentBackupAssetInfoCard(),
Padding(
padding: const EdgeInsets.only(
top: 24,

View File

@@ -11,7 +11,7 @@ import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:openapi/api.dart';
import 'package:photo_manager/src/types/entity.dart';
import 'package:photo_manager/photo_manager.dart';
final assetServiceProvider = Provider(
(ref) => AssetService(

View File

@@ -15,7 +15,7 @@ class ProfileDrawer extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
_buildSignoutButton() {
buildSignoutButton() {
return ListTile(
horizontalTitleGap: 0,
leading: SizedBox(
@@ -46,7 +46,7 @@ class ProfileDrawer extends HookConsumerWidget {
);
}
_buildSettingButton() {
buildSettingButton() {
return ListTile(
horizontalTitleGap: 0,
leading: SizedBox(
@@ -79,8 +79,8 @@ class ProfileDrawer extends HookConsumerWidget {
padding: EdgeInsets.zero,
children: [
const ProfileDrawerHeader(),
_buildSettingButton(),
_buildSignoutButton(),
buildSettingButton(),
buildSignoutButton(),
],
),
const ServerInfoBox()

View File

@@ -25,7 +25,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
var dummmy = Random().nextInt(1024);
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
_buildUserProfileImage() {
buildUserProfileImage() {
if (authState.profileImagePath.isEmpty) {
return const CircleAvatar(
radius: 35,
@@ -77,7 +77,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
return const SizedBox();
}
_pickUserProfileImage() async {
pickUserProfileImage() async {
final XFile? image = await ImagePicker().pickImage(
source: ImageSource.gallery,
maxHeight: 1024,
@@ -98,7 +98,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
useEffect(
() {
_buildUserProfileImage();
buildUserProfileImage();
return null;
},
[],
@@ -129,12 +129,12 @@ class ProfileDrawerHeader extends HookConsumerWidget {
Stack(
clipBehavior: Clip.none,
children: [
_buildUserProfileImage(),
buildUserProfileImage(),
Positioned(
bottom: 0,
right: -5,
child: GestureDetector(
onTap: _pickUserProfileImage,
onTap: pickUserProfileImage,
child: Material(
color: Colors.grey[100],
elevation: 3,

View File

@@ -17,7 +17,7 @@ class ServerInfoBox extends HookConsumerWidget {
final appInfo = useState({});
_getPackageInfo() async {
getPackageInfo() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
appInfo.value = {
@@ -28,7 +28,7 @@ class ServerInfoBox extends HookConsumerWidget {
useEffect(
() {
_getPackageInfo();
getPackageInfo();
return null;
},
[],

View File

@@ -90,6 +90,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
return setSuccessLoginInfo(
accessToken: loginResponse.accessToken,
serverUrl: serverEndpoint,
isSavedLoginInfo: isSavedLoginInfo,
);
} catch (e) {
@@ -159,16 +160,18 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Future<bool> setSuccessLoginInfo({
required String accessToken,
required String serverUrl,
required bool isSavedLoginInfo,
}) async {
Hive.box(userInfoBox).put(accessTokenKey, accessToken);
_apiService.setAccessToken(accessToken);
var userResponseDto = await _apiService.userApi.getMyUserInfo();
if (userResponseDto != null) {
var userInfoHiveBox = await Hive.openBox(userInfoBox);
var deviceInfo = await _deviceInfoService.getDeviceInfo();
Hive.box(userInfoBox).put(deviceIdKey, deviceInfo["deviceId"]);
userInfoHiveBox.put(deviceIdKey, deviceInfo["deviceId"]);
userInfoHiveBox.put(accessTokenKey, accessToken);
userInfoHiveBox.put(serverEndpointKey, serverUrl);
state = state.copyWith(
isAuthenticated: true,
@@ -191,7 +194,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
email: "",
password: "",
isSaveLogin: true,
serverUrl: Hive.box(userInfoBox).get(serverEndpointKey),
serverUrl: serverUrl,
accessToken: accessToken,
),
);

View File

@@ -2,5 +2,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/login/services/oauth.service.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
final OAuthServiceProvider =
final oAuthServiceProvider =
Provider((ref) => OAuthService(ref.watch(apiServiceProvider)));

View File

@@ -349,7 +349,7 @@ class OAuthLoginButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
var oAuthService = ref.watch(OAuthServiceProvider);
var oAuthService = ref.watch(oAuthServiceProvider);
void performOAuthLogin() async {
ref.watch(assetProvider.notifier).clearAllAsset();
@@ -380,6 +380,7 @@ class OAuthLoginButton extends ConsumerWidget {
.setSuccessLoginInfo(
accessToken: loginResponseDto.accessToken,
isSavedLoginInfo: isSavedLoginInfo,
serverUrl: serverEndpointController.text,
);
if (isSuccess) {

View File

@@ -39,14 +39,14 @@ class SearchPage extends HookConsumerWidget {
[],
);
_onSearchSubmitted(String searchTerm) async {
onSearchSubmitted(String searchTerm) async {
searchFocusNode.unfocus();
ref.watch(searchPageStateProvider.notifier).disableSearch();
AutoRouter.of(context).push(SearchResultRoute(searchTerm: searchTerm));
}
_buildPlaces() {
buildPlaces() {
return curatedLocation.when(
loading: () => SizedBox(
height: imageSize,
@@ -97,7 +97,7 @@ class SearchPage extends HookConsumerWidget {
);
}
_buildThings() {
buildThings() {
return curatedObjects.when(
loading: () => SizedBox(
height: imageSize,
@@ -155,7 +155,7 @@ class SearchPage extends HookConsumerWidget {
return Scaffold(
appBar: SearchBar(
searchFocusNode: searchFocusNode,
onSubmitted: _onSearchSubmitted,
onSubmitted: onSearchSubmitted,
),
body: GestureDetector(
onTap: () {
@@ -174,7 +174,7 @@ class SearchPage extends HookConsumerWidget {
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
).tr(),
),
_buildPlaces(),
buildPlaces(),
Padding(
padding: const EdgeInsets.all(16.0),
child: const Text(
@@ -182,11 +182,11 @@ class SearchPage extends HookConsumerWidget {
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
).tr(),
),
_buildThings()
buildThings()
],
),
if (isSearchEnabled)
SearchSuggestionList(onSubmitted: _onSearchSubmitted),
SearchSuggestionList(onSubmitted: onSearchSubmitted),
],
),
),

View File

@@ -38,7 +38,7 @@ class SearchResultPage extends HookConsumerWidget {
[],
);
_onSearchSubmitted(String newSearchTerm) {
onSearchSubmitted(String newSearchTerm) {
debugPrint("Re-Search with $newSearchTerm");
searchFocusNode?.unfocus();
isNewSearch.value = false;
@@ -46,7 +46,7 @@ class SearchResultPage extends HookConsumerWidget {
ref.watch(searchResultPageProvider.notifier).search(newSearchTerm);
}
_buildTextField() {
buildTextField() {
return TextField(
controller: searchTermController,
focusNode: searchFocusNode,
@@ -60,7 +60,7 @@ class SearchResultPage extends HookConsumerWidget {
onSubmitted: (searchTerm) {
if (searchTerm.isNotEmpty) {
searchTermController.clear();
_onSearchSubmitted(searchTerm);
onSearchSubmitted(searchTerm);
} else {
isNewSearch.value = false;
}
@@ -80,7 +80,7 @@ class SearchResultPage extends HookConsumerWidget {
);
}
_buildChip() {
buildChip() {
return Chip(
label: Wrap(
spacing: 5,
@@ -108,7 +108,7 @@ class SearchResultPage extends HookConsumerWidget {
);
}
_buildSearchResult() {
buildSearchResult() {
var searchResultPageState = ref.watch(searchResultPageProvider);
var searchResultRenderList = ref.watch(searchRenderListProvider);
@@ -154,7 +154,7 @@ class SearchResultPage extends HookConsumerWidget {
isNewSearch.value = true;
searchFocusNode?.requestFocus();
},
child: isNewSearch.value ? _buildTextField() : _buildChip(),
child: isNewSearch.value ? buildTextField() : buildChip(),
),
centerTitle: false,
),
@@ -168,9 +168,9 @@ class SearchResultPage extends HookConsumerWidget {
},
child: Stack(
children: [
_buildSearchResult(),
buildSearchResult(),
if (isNewSearch.value)
SearchSuggestionList(onSubmitted: _onSearchSubmitted),
SearchSuggestionList(onSubmitted: onSearchSubmitted),
],
),
),

View File

@@ -4,7 +4,6 @@ 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/asset_list_settings/asset_list_settings.dart';
import 'package:immich_mobile/modules/settings/ui/experimental_settings/experimental_settings.dart';
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';

View File

@@ -39,7 +39,8 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
stopwatch.start();
state = await _assetCacheService.get();
debugPrint(
"Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms");
"Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms",
);
stopwatch.reset();
}
@@ -145,7 +146,9 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
final assetProvider = StateNotifierProvider<AssetNotifier, List<Asset>>((ref) {
return AssetNotifier(
ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider));
ref.watch(assetServiceProvider),
ref.watch(assetCacheServiceProvider),
);
});
final assetGroupByDateTimeProvider = StateProvider((ref) {

View File

@@ -17,10 +17,11 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
try {
String? localReleaseVersion = box.get(githubReleaseInfoKey);
final res = await client.get(
Uri.parse(
"https://api.github.com/repos/immich-app/immich/releases/latest",
),
headers: {"Accept": "application/vnd.github.v3+json"});
Uri.parse(
"https://api.github.com/repos/immich-app/immich/releases/latest",
),
headers: {"Accept": "application/vnd.github.v3+json"},
);
if (res.statusCode == 200) {
final data = jsonDecode(res.body);

View File

@@ -1,3 +1,4 @@
// ignore: depend_on_referenced_packages
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';

View File

@@ -15,7 +15,7 @@ class ImmichToast {
final fToast = FToast();
fToast.init(context);
Color _getColor(ToastType type, BuildContext context) {
Color getColor(ToastType type, BuildContext context) {
switch (type) {
case ToastType.info:
return Theme.of(context).primaryColor;
@@ -26,7 +26,7 @@ class ImmichToast {
}
}
Icon _getIcon(ToastType type) {
Icon getIcon(ToastType type) {
switch (type) {
case ToastType.info:
return Icon(
@@ -60,7 +60,7 @@ class ImmichToast {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_getIcon(toastType),
getIcon(toastType),
const SizedBox(
width: 12.0,
),
@@ -68,7 +68,7 @@ class ImmichToast {
child: Text(
msg,
style: TextStyle(
color: _getColor(toastType, context),
color: getColor(toastType, context),
fontWeight: FontWeight.bold,
fontSize: 15,
),

View File

@@ -20,22 +20,28 @@ class SplashScreenPage extends HookConsumerWidget {
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
void performLoggingIn() async {
if (loginInfo != null) {
// Make sure API service is initialized
apiService.setEndpoint(loginInfo.serverUrl);
try {
if (loginInfo != null) {
// Make sure API service is initialized
apiService.setEndpoint(loginInfo.serverUrl);
var isSuccess =
await ref.read(authenticationProvider.notifier).setSuccessLoginInfo(
accessToken: loginInfo.accessToken,
isSavedLoginInfo: true,
);
if (isSuccess) {
// Resume backup (if enable) then navigate
ref.watch(backupProvider.notifier).resumeBackup();
AutoRouter.of(context).replace(const TabControllerRoute());
} else {
AutoRouter.of(context).replace(const LoginRoute());
var isSuccess = await ref
.read(authenticationProvider.notifier)
.setSuccessLoginInfo(
accessToken: loginInfo.accessToken,
isSavedLoginInfo: true,
serverUrl: loginInfo.serverUrl,
);
if (isSuccess) {
// Resume backup (if enable) then navigate
ref.watch(backupProvider.notifier).resumeBackup();
AutoRouter.of(context).replace(const TabControllerRoute());
} else {
AutoRouter.of(context).replace(const LoginRoute());
}
}
} catch (_) {
AutoRouter.of(context).replace(const LoginRoute());
}
}

View File

@@ -1,3 +1,5 @@
// ignore_for_file: depend_on_referenced_packages, implementation_imports
import 'dart:io';
import 'dart:math';

View File

@@ -79,71 +79,74 @@ class AssetResponseDto {
String? livePhotoVideoId;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
other.type == type &&
other.id == id &&
other.deviceAssetId == deviceAssetId &&
other.ownerId == ownerId &&
other.deviceId == deviceId &&
other.originalPath == originalPath &&
other.resizePath == resizePath &&
other.createdAt == createdAt &&
other.modifiedAt == modifiedAt &&
other.isFavorite == isFavorite &&
other.mimeType == mimeType &&
other.duration == duration &&
other.webpPath == webpPath &&
other.encodedVideoPath == encodedVideoPath &&
other.exifInfo == exifInfo &&
other.smartInfo == smartInfo &&
other.livePhotoVideoId == livePhotoVideoId;
bool operator ==(Object other) =>
identical(this, other) ||
other is AssetResponseDto &&
other.type == type &&
other.id == id &&
other.deviceAssetId == deviceAssetId &&
other.ownerId == ownerId &&
other.deviceId == deviceId &&
other.originalPath == originalPath &&
other.resizePath == resizePath &&
other.createdAt == createdAt &&
other.modifiedAt == modifiedAt &&
other.isFavorite == isFavorite &&
other.mimeType == mimeType &&
other.duration == duration &&
other.webpPath == webpPath &&
other.encodedVideoPath == encodedVideoPath &&
other.exifInfo == exifInfo &&
other.smartInfo == smartInfo &&
other.livePhotoVideoId == livePhotoVideoId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(type.hashCode) +
(id.hashCode) +
(deviceAssetId.hashCode) +
(ownerId.hashCode) +
(deviceId.hashCode) +
(originalPath.hashCode) +
(resizePath == null ? 0 : resizePath!.hashCode) +
(createdAt.hashCode) +
(modifiedAt.hashCode) +
(isFavorite.hashCode) +
(mimeType == null ? 0 : mimeType!.hashCode) +
(duration.hashCode) +
(webpPath == null ? 0 : webpPath!.hashCode) +
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
(exifInfo == null ? 0 : exifInfo!.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode) +
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode);
// ignore: unnecessary_parenthesis
(type.hashCode) +
(id.hashCode) +
(deviceAssetId.hashCode) +
(ownerId.hashCode) +
(deviceId.hashCode) +
(originalPath.hashCode) +
(resizePath == null ? 0 : resizePath!.hashCode) +
(createdAt.hashCode) +
(modifiedAt.hashCode) +
(isFavorite.hashCode) +
(mimeType == null ? 0 : mimeType!.hashCode) +
(duration.hashCode) +
(webpPath == null ? 0 : webpPath!.hashCode) +
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
(exifInfo == null ? 0 : exifInfo!.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode) +
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode);
@override
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, livePhotoVideoId=$livePhotoVideoId]';
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, livePhotoVideoId=$livePhotoVideoId]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'type'] = type;
_json[r'id'] = id;
_json[r'deviceAssetId'] = deviceAssetId;
_json[r'ownerId'] = ownerId;
_json[r'deviceId'] = deviceId;
_json[r'originalPath'] = originalPath;
_json[r'type'] = type;
_json[r'id'] = id;
_json[r'deviceAssetId'] = deviceAssetId;
_json[r'ownerId'] = ownerId;
_json[r'deviceId'] = deviceId;
_json[r'originalPath'] = originalPath;
if (resizePath != null) {
_json[r'resizePath'] = resizePath;
} else {
_json[r'resizePath'] = null;
}
_json[r'createdAt'] = createdAt;
_json[r'modifiedAt'] = modifiedAt;
_json[r'isFavorite'] = isFavorite;
_json[r'createdAt'] = createdAt;
_json[r'modifiedAt'] = modifiedAt;
_json[r'isFavorite'] = isFavorite;
if (mimeType != null) {
_json[r'mimeType'] = mimeType;
} else {
_json[r'mimeType'] = null;
}
_json[r'duration'] = duration;
_json[r'duration'] = duration;
if (webpPath != null) {
_json[r'webpPath'] = webpPath;
} else {
@@ -182,13 +185,13 @@ class AssetResponseDto {
// Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode!
assert(() {
requiredKeys.forEach((key) {
assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
});
return true;
}());
// assert(() {
// requiredKeys.forEach((key) {
// assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
// assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
// });
// return true;
// }());
return AssetResponseDto(
type: AssetTypeEnum.fromJson(json[r'type'])!,
@@ -213,7 +216,10 @@ class AssetResponseDto {
return null;
}
static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
static List<AssetResponseDto>? listFromJson(
dynamic json, {
bool growable = false,
}) {
final result = <AssetResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
@@ -241,12 +247,18 @@ class AssetResponseDto {
}
// maps a json object with a list of AssetResponseDto-objects as value to a dart map
static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
static Map<String, List<AssetResponseDto>> mapListFromJson(
dynamic json, {
bool growable = false,
}) {
final map = <String, List<AssetResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
final value = AssetResponseDto.listFromJson(
entry.value,
growable: growable,
);
if (value != null) {
map[entry.key] = value;
}
@@ -274,4 +286,3 @@ class AssetResponseDto {
'livePhotoVideoId',
};
}

View File

@@ -43,43 +43,46 @@ class UserResponseDto {
DateTime? deletedAt;
@override
bool operator ==(Object other) => identical(this, other) || other is UserResponseDto &&
other.id == id &&
other.email == email &&
other.firstName == firstName &&
other.lastName == lastName &&
other.createdAt == createdAt &&
other.profileImagePath == profileImagePath &&
other.shouldChangePassword == shouldChangePassword &&
other.isAdmin == isAdmin &&
other.deletedAt == deletedAt;
bool operator ==(Object other) =>
identical(this, other) ||
other is UserResponseDto &&
other.id == id &&
other.email == email &&
other.firstName == firstName &&
other.lastName == lastName &&
other.createdAt == createdAt &&
other.profileImagePath == profileImagePath &&
other.shouldChangePassword == shouldChangePassword &&
other.isAdmin == isAdmin &&
other.deletedAt == deletedAt;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(id.hashCode) +
(email.hashCode) +
(firstName.hashCode) +
(lastName.hashCode) +
(createdAt.hashCode) +
(profileImagePath.hashCode) +
(shouldChangePassword.hashCode) +
(isAdmin.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode);
// ignore: unnecessary_parenthesis
(id.hashCode) +
(email.hashCode) +
(firstName.hashCode) +
(lastName.hashCode) +
(createdAt.hashCode) +
(profileImagePath.hashCode) +
(shouldChangePassword.hashCode) +
(isAdmin.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode);
@override
String toString() => 'UserResponseDto[id=$id, email=$email, firstName=$firstName, lastName=$lastName, createdAt=$createdAt, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, isAdmin=$isAdmin, deletedAt=$deletedAt]';
String toString() =>
'UserResponseDto[id=$id, email=$email, firstName=$firstName, lastName=$lastName, createdAt=$createdAt, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, isAdmin=$isAdmin, deletedAt=$deletedAt]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'id'] = id;
_json[r'email'] = email;
_json[r'firstName'] = firstName;
_json[r'lastName'] = lastName;
_json[r'createdAt'] = createdAt;
_json[r'profileImagePath'] = profileImagePath;
_json[r'shouldChangePassword'] = shouldChangePassword;
_json[r'isAdmin'] = isAdmin;
_json[r'id'] = id;
_json[r'email'] = email;
_json[r'firstName'] = firstName;
_json[r'lastName'] = lastName;
_json[r'createdAt'] = createdAt;
_json[r'profileImagePath'] = profileImagePath;
_json[r'shouldChangePassword'] = shouldChangePassword;
_json[r'isAdmin'] = isAdmin;
if (deletedAt != null) {
_json[r'deletedAt'] = deletedAt!.toUtc().toIso8601String();
} else {
@@ -98,13 +101,13 @@ class UserResponseDto {
// Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode!
assert(() {
requiredKeys.forEach((key) {
assert(json.containsKey(key), 'Required key "UserResponseDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "UserResponseDto[$key]" has a null value in JSON.');
});
return true;
}());
// assert(() {
// requiredKeys.forEach((key) {
// assert(json.containsKey(key), 'Required key "UserResponseDto[$key]" is missing from JSON.');
// assert(json[key] != null, 'Required key "UserResponseDto[$key]" has a null value in JSON.');
// });
// return true;
// }());
return UserResponseDto(
id: mapValueOfType<String>(json, r'id')!,
@@ -113,7 +116,8 @@ class UserResponseDto {
lastName: mapValueOfType<String>(json, r'lastName')!,
createdAt: mapValueOfType<String>(json, r'createdAt')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
shouldChangePassword:
mapValueOfType<bool>(json, r'shouldChangePassword')!,
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
deletedAt: mapDateTime(json, r'deletedAt', ''),
);
@@ -121,7 +125,10 @@ class UserResponseDto {
return null;
}
static List<UserResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
static List<UserResponseDto>? listFromJson(
dynamic json, {
bool growable = false,
}) {
final result = <UserResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
@@ -149,12 +156,18 @@ class UserResponseDto {
}
// maps a json object with a list of UserResponseDto-objects as value to a dart map
static Map<String, List<UserResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
static Map<String, List<UserResponseDto>> mapListFromJson(
dynamic json, {
bool growable = false,
}) {
final map = <String, List<UserResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = UserResponseDto.listFromJson(entry.value, growable: growable,);
final value = UserResponseDto.listFromJson(
entry.value,
growable: growable,
);
if (value != null) {
map[entry.key] = value;
}
@@ -176,4 +189,3 @@ class UserResponseDto {
'deletedAt',
};
}

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.36.0+55
version: 1.36.1+56
environment:
sdk: ">=2.17.0 <3.0.0"

View File

@@ -18,7 +18,14 @@ export class AlbumResponseDto {
}
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
const sharedUsers = entity.sharedUsers?.map((userAlbum) => mapUser(userAlbum.userInfo)) || [];
const sharedUsers: UserResponseDto[] = [];
entity.sharedUsers?.forEach((userAlbum) => {
if (userAlbum.userInfo) {
const user = mapUser(userAlbum.userInfo);
sharedUsers.push(user);
}
});
return {
albumName: entity.albumName,
albumThumbnailAssetId: entity.albumThumbnailAssetId,
@@ -33,7 +40,14 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
}
export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto {
const sharedUsers = entity.sharedUsers?.map((userAlbum) => mapUser(userAlbum.userInfo)) || [];
const sharedUsers: UserResponseDto[] = [];
entity.sharedUsers?.forEach((userAlbum) => {
if (userAlbum.userInfo) {
const user = mapUser(userAlbum.userInfo);
sharedUsers.push(user);
}
});
return {
albumName: entity.albumName,
albumThumbnailAssetId: entity.albumThumbnailAssetId,

View File

@@ -59,7 +59,6 @@ export class UserRepository implements IUserRepository {
user.salt = await bcrypt.genSalt();
user.password = await this.hashPassword(user.password, user.salt);
}
user.isAdmin = false;
return this.userRepository.save(user);
}

View File

@@ -11,6 +11,6 @@ export interface IServerVersion {
export const serverVersion: IServerVersion = {
major: 1,
minor: 36,
patch: 0,
build: 55,
patch: 2,
build: 56,
};