Compare commits

...

26 Commits

Author SHA1 Message Date
Alex Tran
e84c705e31 Added changelog to Fdroid 2022-07-03 10:49:37 -05:00
Alex Tran
36162509e0 Up version for release 2022-07-03 10:39:09 -05:00
Alex
76bf1c0379 Remove thumbnail generation on mobile app (#292)
* Remove thumbnail generation on mobile

* Remove tconditions for missing thumbnail on the backend

* Remove console.log

* Refactor queue systems

* Convert queue and processor name to constant

* Added corresponding interface to job queue
2022-07-02 21:06:36 -05:00
Alex
32b847c26e Fixed event propagation trigger navigating twice (#293) 2022-07-01 20:49:41 -05:00
Alex
a45d6fdf57 Fix server crash on bad file operation and other optimizations (#291)
* Fixed issue with generating thumbnail for video with 0 length cause undefined file and crash the server
* Added all file error handling operation
* Temporarily disabled WebSocket on the web because receiving a new upload event doesn't put the new file in the correct place. 
* Cosmetic fixed on the info panel
2022-07-01 12:00:12 -05:00
Zack Pollard
c071e64a7e infra: switch port to 3003 for machine learning container (#290)
* infra: switch port to 3003 for machine learning container

fixes #289

* Changed port of machine-learning-endpoint to match with new port

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-07-01 10:20:04 -05:00
Alex
663f12851e Fixed filename duplication when upload from web (#288)
* Fixed filename duplication when upload from web

* Fixed cosmetic of detail panel view
2022-06-30 20:43:33 -05:00
xpwmaosldk
c4ef523564 Optimize mobile - Avoid creating unnecessary widgets (#268)
* Avoid creating unnecessary widgets

* more flexible null handling and runtime errors prevention
2022-06-30 20:08:49 -05:00
Alex
992f792c0a Fixed admin is forced to change password on mobile app (#287)
* Fixed issues

* Upversion and add changed log
2022-06-30 13:59:02 -05:00
Alex Tran
97611fa057 Fixed issue with unexposed Nginx port on release image 2022-06-30 00:26:54 -05:00
Alex Tran
32240777c3 fixed release build directory for Github action 2022-06-30 00:10:01 -05:00
Alex Tran
6065ff8caa Update readme with new discord invitation link 2022-06-29 23:50:24 -05:00
Alex Tran
8db073941d Up server version for release 2022-06-29 21:54:57 -05:00
Alex
5e281b44e9 Add Podman Support (#278) 2022-06-29 21:49:35 -05:00
Zack Pollard
142ede350e feat: create immich-nginx container to remove default nginx config setup (#280)
* feat: create immich-proxy container to remove default nginx config setup

* infra: make production docker-compose point at release builds for stability

* Fixed nginx config file was overriden by default.conf in nginx container; Fixed docker-compose.dev; Added additional tag 'release' for tagging after release build in Github Action

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-06-29 21:24:55 -05:00
Alex
a2e1d4caa2 Update server dependencies and fixed Typeorm API changes in new version (#276)
* Fixed dependencies

* Adapt typeorm API to be compatible with new version

* Fixed typeorm API in tests

* Remove console.log
2022-06-29 13:39:58 -05:00
Alex
5f00d8b9c6 Added mechanism of required password change of new user's first login (#272)
* Deprecate login scenarios that support pre-web era

* refactor and simplify setup

* Added user info to change password form

* change isFistLogin column to shouldChangePassword

* Implemented change user password

* Implement the change password page for mobile

* Change label

* Added changes log and up minor version

* Fixed typo in the release note

* Up server version
2022-06-27 15:13:07 -05:00
bo0tzz
2e85e18020 Use APP_UPLOAD_LOCATION constant for diskInfo (#271) 2022-06-27 09:14:32 -05:00
Alex
40a8115101 Fix backup not resuming after closed and reopen (#266)
* Fixed app not resuming backup after closing and reopening the app

* Fixed cosmetic effect of backup button doesn't change state right away after pressing start backup

* Fixed grammar

* Fixed deep copy problem that cause incorrect asset count when backing up

* Format code
2022-06-25 15:12:47 -05:00
xpwmaosldk
d02b97e1c1 Add service provider (#250)
* optimize android side gradle settings

* android minsdk back to 21

* remove unused package, update linter and fix lint error

* clean code of 'shared module' with offical dart style guide

* restore uploadProfileImage method in UserService

* add service provider

* fix searchFocusNode init error
2022-06-25 13:46:51 -05:00
Alex Tran
485b152beb Improved info panel on web 2022-06-25 13:28:36 -05:00
Jaime Baez
c918f5b001 Set TypeScript to strict mode and fix issues related to server types (#261)
* Fix lint issues and some other TS issues

- set TypeScript in strict mode
- add npm commands to lint / check code
- fix all lint issues
- fix some TS issues
- rename User reponse DTO to make it consistent with the other ones
- override Express/User interface to use UserResponseDto interface
 This is for when the accessing the `user` from a Express Request,
 like in `asset-upload-config`

* Fix the rest of TS issues

- fix all the remaining TypeScript errors
- add missing `@types/mapbox__mapbox-sdk` package

* Move global.d.ts to server `src` folder

* Update AssetReponseDto duration type

This is now of type `string` that defaults to '0:00:00.00000' if not set
which is what the mobile app currently expects

* Set context when logging error in asset.service

Use `ServeFile` as the context for logging an error when
asset.resizePath is not set

* Fix wrong AppController merge conflict resolution

`redirectToWebpage` was removed in main as is no longer used.
2022-06-25 12:53:06 -05:00
Jaime Baez
cca2f7d178 Fix web container port mapping (#264)
Vite uses port 3000 to do Websocket connection. If it doesn't mange to
connect it enters in an endless loop refreshing the page.
2022-06-25 12:30:38 -05:00
Matthias Rupp
baf533de35 Delete assets from server if local deletion fails (#260)
* Delete assets from server if local deletion fails

* Remove commented line
2022-06-24 10:02:09 -05:00
Alex
dfc0d6eee7 Update readme with additional image 2022-06-24 07:28:56 -05:00
Alex Tran
7948cb8110 remove redirect from previous implementation of web routing 2022-06-24 06:33:11 -05:00
219 changed files with 4189 additions and 3207 deletions

View File

@@ -91,3 +91,30 @@ jobs:
push: true
tags: |
altran1502/immich-web:latest
build_and_push_nginx_latest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Proxy
uses: docker/build-push-action@v3.0.0
with:
context: ./nginx
file: ./nginx/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: true
tags: |
altran1502/immich-proxy:latest

View File

@@ -93,3 +93,30 @@ jobs:
push: ${{ github.event_name == 'pull_request' }}
tags: |
altran1502/immich-web:staging
build_and_push_nginx_staging:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Proxy
uses: docker/build-push-action@v3.0.0
with:
context: ./nginx
file: ./nginx/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' }}
tags: |
altran1502/immich-proxy:staging

View File

@@ -43,6 +43,7 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: |
altran1502/immich-server:${{ steps.previoustag.outputs.tag }}
altran1502/immich-server:release
build_and_push_machine_learning_release:
runs-on: ubuntu-latest
@@ -75,6 +76,7 @@ jobs:
push: true
tags: |
altran1502/immich-machine-learning:${{ steps.previoustag.outputs.tag }}
altran1502/immich-machine-learning:release
build_and_push_web_release:
runs-on: ubuntu-latest
@@ -114,3 +116,43 @@ jobs:
target: prod
tags: |
altran1502/immich-web:${{ steps.previoustag.outputs.tag }}
altran1502/immich-web:release
build_and_push_nginx_release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "main"
fetch-depth: 0
- name: "Get Previous tag"
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-proxy release
uses: docker/build-push-action@v3.0.0
with:
context: ./nginx
file: ./nginx/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: |
altran1502/immich-proxy:release
altran1502/immich-proxy:${{ steps.previoustag.outputs.tag }}

View File

@@ -10,7 +10,7 @@
<a href="https://actions-badge.atrox.dev/alextran1502/immich/goto?ref=main">
<img alt="Build Status" src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Falextran1502%2Fimmich%2Fbadge%3Fref%3Dmain&style=for-the-badge&label=Github Action&logo=github&labelColor=ececec&logoColor=000000" />
</a>
<a href="https://discord.gg/rxnyVTXGbM">
<a href="https://discord.gg/D8JsnBEuKb">
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Immich%20Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Immich Discord"/>
</a>
<br/>
@@ -185,7 +185,11 @@ Additional accounts on the server can be created by the admin account.
## Step 4: Run mobile app
The app is distributed on several platforms below.
Login the mobile app with your server address
<p align="left">
<img src="design/login-screen.jpeg" width="250" title="Example login screen">
<p/>
## F-Droid
You can get the app on F-droid by clicking the image below.

BIN
design/login-screen.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -7,8 +7,6 @@ services:
context: ../server
dockerfile: Dockerfile
command: npm run start:dev immich
expose:
- "3000"
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
@@ -27,8 +25,6 @@ services:
context: ../machine-learning
dockerfile: Dockerfile
command: npm run start:dev
expose:
- "3001"
volumes:
- ../machine-learning:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
@@ -68,7 +64,7 @@ services:
env_file:
- .env
ports:
- 3002:3000
- 3000:3000
- 24678:24678
volumes:
- ../web:/usr/src/app
@@ -94,11 +90,12 @@ services:
ports:
- 5432:5432
nginx:
container_name: proxy_nginx
image: nginx:latest
volumes:
- ./settings/nginx-conf:/etc/nginx/conf.d
immich-proxy:
container_name: immich_proxy
image: immich-proxy-dev:latest
build:
context: ../nginx
dockerfile: Dockerfile
ports:
- 2283:80
- 2284:443
@@ -106,6 +103,7 @@ services:
driver: none
depends_on:
- immich-server
restart: always
volumes:
pgdata:

View File

@@ -68,11 +68,9 @@ services:
- pgdata:/var/lib/postgresql/data
restart: always
nginx:
container_name: proxy_nginx
image: nginx:latest
volumes:
- ./settings/nginx-conf:/etc/nginx/conf.d
immich-proxy:
container_name: immich_proxy
image: altran1502/immich-proxy:staging
ports:
- 2283:80
- 2284:443

View File

@@ -2,7 +2,7 @@ version: "3.8"
services:
immich-server:
image: altran1502/immich-server:latest
image: altran1502/immich-server:release
entrypoint: ["/bin/sh", "./start-server.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
@@ -16,7 +16,7 @@ services:
restart: always
immich-microservices:
image: altran1502/immich-server:latest
image: altran1502/immich-server:release
entrypoint: ["/bin/sh", "./start-microservices.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
@@ -30,7 +30,7 @@ services:
restart: always
immich-machine-learning:
image: altran1502/immich-machine-learning:latest
image: altran1502/immich-machine-learning:release
entrypoint: ["/bin/sh", "./entrypoint.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
@@ -43,7 +43,7 @@ services:
restart: always
immich-web:
image: altran1502/immich-web:latest
image: altran1502/immich-web:release
entrypoint: ["/bin/sh", "./entrypoint.sh"]
env_file:
- .env
@@ -68,11 +68,9 @@ services:
- pgdata:/var/lib/postgresql/data
restart: always
nginx:
container_name: proxy_nginx
image: nginx:latest
volumes:
- ./settings/nginx-conf:/etc/nginx/conf.d
immich-proxy:
container_name: immich_proxy
image: altran1502/immich-proxy:release
ports:
- 2283:80
- 2284:443

View File

@@ -5,7 +5,7 @@ import { Logger } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3001, () => {
await app.listen(3003, () => {
if (process.env.NODE_ENV == 'development') {
Logger.log(
'Running Immich Machine Learning in DEVELOPMENT environment',

View File

@@ -0,0 +1,3 @@
* Fixed app does not resume back up when reopening a closed app
* Fixed wrong asset count on the upload page
* Added mechanism to change the password of new user on the first login (except Admin)

View File

@@ -0,0 +1,2 @@
* Fixed admin is forced to change password upon logging in on mobile app
* Fixed change password form validation

View File

@@ -0,0 +1 @@
* Removed thumbnail generation on mobile - the operation now will be on the server to reduce CPU load and battery usage.

View File

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

View File

@@ -28,22 +28,26 @@ class ImageViewerPageState {
factory ImageViewerPageState.fromMap(Map<String, dynamic> map) {
return ImageViewerPageState(
downloadAssetStatus: DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0],
downloadAssetStatus:
DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0],
);
}
String toJson() => json.encode(toMap());
factory ImageViewerPageState.fromJson(String source) => ImageViewerPageState.fromMap(json.decode(source));
factory ImageViewerPageState.fromJson(String source) =>
ImageViewerPageState.fromMap(json.decode(source));
@override
String toString() => 'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)';
String toString() =>
'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ImageViewerPageState && other.downloadAssetStatus == downloadAssetStatus;
return other is ImageViewerPageState &&
other.downloadAssetStatus == downloadAssetStatus;
}
@override

View File

@@ -9,7 +9,9 @@ import 'package:immich_mobile/shared/ui/immich_toast.dart';
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
final ImageViewerService _imageViewerService = ImageViewerService();
ImageViewerStateNotifier() : super(ImageViewerPageState(downloadAssetStatus: DownloadAssetStatus.idle));
ImageViewerStateNotifier()
: super(ImageViewerPageState(
downloadAssetStatus: DownloadAssetStatus.idle));
void downloadAsset(ImmichAsset asset, BuildContext context) async {
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
@@ -40,4 +42,5 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
}
final imageViewerStateProvider =
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(((ref) => ImageViewerStateNotifier()));
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
((ref) => ImageViewerStateNotifier()));

View File

@@ -15,12 +15,14 @@ class ImageViewerService {
try {
String fileName = p.basename(asset.originalPath);
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
Uri filePath =
Uri.parse("$savedEndpoint/asset/download?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false");
Uri filePath = Uri.parse(
"$savedEndpoint/asset/download?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false");
var res = await http.get(
filePath,
headers: {"Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}"},
headers: {
"Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}"
},
);
final AssetEntity? entity;
@@ -37,14 +39,10 @@ class ImageViewerService {
entity = await PhotoManager.editor.saveVideo(tempFile, title: fileName);
}
if (entity != null) {
return true;
}
return entity != null;
} catch (e) {
debugPrint("Error saving file $e");
return false;
}
return false;
}
}

View File

@@ -15,9 +15,7 @@ class ExifBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
_buildMap() {
return (assetDetail.exifInfo!.latitude != null &&
assetDetail.exifInfo!.longitude != null)
? Padding(
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Container(
height: 150,
@@ -57,29 +55,23 @@ class ExifBottomSheet extends ConsumerWidget {
],
),
),
)
: Container();
);
}
_buildLocationText() {
return (assetDetail.exifInfo!.city != null &&
assetDetail.exifInfo!.state != null)
? Text(
return Text(
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
style: TextStyle(
fontSize: 12,
color: Colors.grey[200],
fontWeight: FontWeight.bold),
)
: Container();
fontSize: 12, color: Colors.grey[200], fontWeight: FontWeight.bold),
);
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
child: ListView(
children: [
assetDetail.exifInfo?.dateTimeOriginal != null
? Text(
if (assetDetail.exifInfo?.dateTimeOriginal != null)
Text(
DateFormat('E, LLL d, y • h:mm a').format(
DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!),
),
@@ -88,8 +80,7 @@ class ExifBottomSheet extends ConsumerWidget {
fontWeight: FontWeight.bold,
fontSize: 14,
),
)
: Container(),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Text(
@@ -102,8 +93,8 @@ class ExifBottomSheet extends ConsumerWidget {
),
// Location
assetDetail.exifInfo?.latitude != null
? Padding(
if (assetDetail.exifInfo?.latitude != null)
Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -116,19 +107,22 @@ class ExifBottomSheet extends ConsumerWidget {
"LOCATION",
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
),
if (assetDetail.exifInfo?.latitude != null &&
assetDetail.exifInfo?.longitude != null)
_buildMap(),
if (assetDetail.exifInfo?.city != null &&
assetDetail.exifInfo?.state != null)
_buildLocationText(),
Text(
"${assetDetail.exifInfo!.latitude!.toStringAsFixed(4)}, ${assetDetail.exifInfo!.longitude!.toStringAsFixed(4)}",
"${assetDetail.exifInfo?.latitude?.toStringAsFixed(4)}, ${assetDetail.exifInfo?.longitude?.toStringAsFixed(4)}",
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
)
],
),
)
: Container(),
),
// Detail
assetDetail.exifInfo != null
? Padding(
if (assetDetail.exifInfo != null)
Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -141,8 +135,7 @@ class ExifBottomSheet extends ConsumerWidget {
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"DETAILS",
style:
TextStyle(fontSize: 11, color: Colors.grey[400]),
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
),
),
ListTile(
@@ -158,10 +151,10 @@ class ExifBottomSheet extends ConsumerWidget {
subtitle: assetDetail.exifInfo?.exifImageHeight != null
? Text(
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ")
: Container(),
: null,
),
assetDetail.exifInfo?.make != null
? ListTile(
if (assetDetail.exifInfo?.make != null)
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
textColor: Colors.grey[300],
@@ -169,17 +162,14 @@ class ExifBottomSheet extends ConsumerWidget {
leading: const Icon(Icons.camera),
title: Text(
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
style: const TextStyle(
fontWeight: FontWeight.bold),
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / assetDetail.exifInfo!.exposureTime!).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} "),
)
: Container()
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} "),
),
],
),
)
: Container()
),
],
),
);

View File

@@ -20,20 +20,26 @@ class ImageViewerPage extends HookConsumerWidget {
final String heroTag;
final String thumbnailUrl;
final ImmichAsset asset;
final AssetService _assetService = AssetService();
ImmichAssetWithExif? assetDetail;
ImageViewerPage(
{Key? key, required this.imageUrl, required this.heroTag, required this.thumbnailUrl, required this.asset})
: super(key: key);
ImageViewerPage({
Key? key,
required this.imageUrl,
required this.heroTag,
required this.thumbnailUrl,
required this.asset,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
final downloadAssetStatus =
ref.watch(imageViewerStateProvider).downloadAssetStatus;
var box = Hive.box(userInfoBox);
getAssetExif() async {
assetDetail = await _assetService.getAssetById(asset.id);
assetDetail =
await ref.watch(assetServiceProvider).getAssetById(asset.id);
}
showInfo() {
@@ -59,7 +65,9 @@ class ImageViewerPage extends HookConsumerWidget {
asset: asset,
onMoreInfoPressed: showInfo,
onDownloadPressed: () {
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
ref
.watch(imageViewerStateProvider.notifier)
.downloadAsset(asset, context);
},
),
body: SafeArea(
@@ -74,8 +82,7 @@ class ImageViewerPage extends HookConsumerWidget {
authToken: "Bearer ${box.get(accessTokenKey)}",
onSwipeDown: () => AutoRouter.of(context).pop(),
onSwipeUp: () => showInfo(),
)
),
)),
),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(

View File

@@ -21,13 +21,14 @@ class VideoViewerPage extends HookConsumerWidget {
final String videoUrl;
final ImmichAsset asset;
ImmichAssetWithExif? assetDetail;
final AssetService _assetService = AssetService();
VideoViewerPage({Key? key, required this.videoUrl, required this.asset}) : super(key: key);
VideoViewerPage({Key? key, required this.videoUrl, required this.asset})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
final downloadAssetStatus =
ref.watch(imageViewerStateProvider).downloadAssetStatus;
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
@@ -44,7 +45,8 @@ class VideoViewerPage extends HookConsumerWidget {
}
getAssetExif() async {
assetDetail = await _assetService.getAssetById(asset.id);
assetDetail =
await ref.watch(assetServiceProvider).getAssetById(asset.id);
}
useEffect(() {
@@ -60,7 +62,9 @@ class VideoViewerPage extends HookConsumerWidget {
showInfo();
},
onDownloadPressed: () {
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
ref
.watch(imageViewerStateProvider.notifier)
.downloadAsset(asset, context);
},
),
body: SwipeDetector(
@@ -93,7 +97,8 @@ class VideoThumbnailPlayer extends StatefulWidget {
final String url;
final String? jwtToken;
const VideoThumbnailPlayer({Key? key, required this.url, this.jwtToken}) : super(key: key);
const VideoThumbnailPlayer({Key? key, required this.url, this.jwtToken})
: super(key: key);
@override
State<VideoThumbnailPlayer> createState() => _VideoThumbnailPlayerState();
@@ -111,8 +116,8 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
Future<void> initializePlayer() async {
try {
videoPlayerController =
VideoPlayerController.network(widget.url, httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"});
videoPlayerController = VideoPlayerController.network(widget.url,
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"});
await videoPlayerController.initialize();
_createChewieController();
@@ -142,7 +147,7 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
@override
Widget build(BuildContext context) {
return chewieController != null && chewieController!.videoPlayerController.value.isInitialized
return chewieController?.videoPlayerController.value.isInitialized == true
? SizedBox(
child: Chewie(
controller: chewieController!,

View File

@@ -21,13 +21,16 @@ class AvailableAlbum {
}
@override
String toString() => 'AvailableAlbum(albumEntity: $albumEntity, thumbnailData: $thumbnailData)';
String toString() =>
'AvailableAlbum(albumEntity: $albumEntity, thumbnailData: $thumbnailData)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is AvailableAlbum && other.albumEntity == albumEntity && other.thumbnailData == thumbnailData;
return other is AvailableAlbum &&
other.albumEntity == albumEntity &&
other.thumbnailData == thumbnailData;
}
@override

View File

@@ -10,7 +10,7 @@ enum BackUpProgressEnum { idle, inProgress, done }
class BackUpState extends Equatable {
// enum
final BackUpProgressEnum backupProgress;
final List<String> allAssetOnDatabase;
final List<String> allAssetsInDatabase;
final double progressInPercentage;
final CancellationToken cancelToken;
final ServerInfo serverInfo;
@@ -28,7 +28,7 @@ class BackUpState extends Equatable {
const BackUpState({
required this.backupProgress,
required this.allAssetOnDatabase,
required this.allAssetsInDatabase,
required this.progressInPercentage,
required this.cancelToken,
required this.serverInfo,
@@ -41,7 +41,7 @@ class BackUpState extends Equatable {
BackUpState copyWith({
BackUpProgressEnum? backupProgress,
List<String>? allAssetOnDatabase,
List<String>? allAssetsInDatabase,
double? progressInPercentage,
CancellationToken? cancelToken,
ServerInfo? serverInfo,
@@ -53,7 +53,7 @@ class BackUpState extends Equatable {
}) {
return BackUpState(
backupProgress: backupProgress ?? this.backupProgress,
allAssetOnDatabase: allAssetOnDatabase ?? this.allAssetOnDatabase,
allAssetsInDatabase: allAssetsInDatabase ?? this.allAssetsInDatabase,
progressInPercentage: progressInPercentage ?? this.progressInPercentage,
cancelToken: cancelToken ?? this.cancelToken,
serverInfo: serverInfo ?? this.serverInfo,
@@ -61,20 +61,21 @@ class BackUpState extends Equatable {
selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
selectedAlbumsBackupAssetsIds:
selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
);
}
@override
String toString() {
return 'BackUpState(backupProgress: $backupProgress, allAssetOnDatabase: $allAssetOnDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds)';
return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds)';
}
@override
List<Object> get props {
return [
backupProgress,
allAssetOnDatabase,
allAssetsInDatabase,
progressInPercentage,
cancelToken,
serverInfo,

View File

@@ -19,7 +19,8 @@ class HiveBackupAlbums {
});
@override
String toString() => 'HiveBackupAlbums(selectedAlbumIds: $selectedAlbumIds, excludedAlbumsIds: $excludedAlbumsIds)';
String toString() =>
'HiveBackupAlbums(selectedAlbumIds: $selectedAlbumIds, excludedAlbumsIds: $excludedAlbumsIds)';
HiveBackupAlbums copyWith({
List<String>? selectedAlbumIds,
@@ -49,7 +50,8 @@ class HiveBackupAlbums {
String toJson() => json.encode(toMap());
factory HiveBackupAlbums.fromJson(String source) => HiveBackupAlbums.fromMap(json.decode(source));
factory HiveBackupAlbums.fromJson(String source) =>
HiveBackupAlbums.fromMap(json.decode(source));
@override
bool operator ==(Object other) {

View File

@@ -7,17 +7,18 @@ import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/services/backup.service.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.model.dart';
import 'package:immich_mobile/shared/services/server_info.service.dart';
import 'package:photo_manager/photo_manager.dart';
class BackupNotifier extends StateNotifier<BackUpState> {
BackupNotifier({this.ref})
BackupNotifier(this._backupService, this._serverInfoService, this._authState)
: super(
BackUpState(
backupProgress: BackUpProgressEnum.idle,
allAssetOnDatabase: const [],
allAssetsInDatabase: const [],
progressInPercentage: 0,
cancelToken: CancellationToken(),
serverInfo: ServerInfo(
@@ -35,11 +36,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
allUniqueAssets: const {},
selectedAlbumsBackupAssetsIds: const {},
),
);
) {
getBackupInfo();
}
Ref? ref;
final BackupService _backupService = BackupService();
final ServerInfoService _serverInfoService = ServerInfoService();
final BackupService _backupService;
final ServerInfoService _serverInfoService;
final AuthenticationState _authState;
///
/// UI INTERACTION
@@ -92,7 +95,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// If this is the first time performing backup - set the default selected album to be
/// the one that has all assets (Recent on Android, Recents on iOS)
///
Future<void> getBackupAlbumsInfo() async {
Future<void> _getBackupAlbumsInfo() async {
// Get all albums on the device
List<AvailableAlbum> availableAlbums = [];
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
@@ -176,7 +179,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// Find the assets that are not overlapping between the two sets
/// Those assets are unique and are used as the total assets
///
void _updateBackupAssetCount() async {
Future<void> _updateBackupAssetCount() async {
Set<AssetEntity> assetsFromSelectedAlbums = {};
Set<AssetEntity> assetsFromExcludedAlbums = {};
@@ -194,27 +197,27 @@ class BackupNotifier extends StateNotifier<BackUpState> {
Set<AssetEntity> allUniqueAssets =
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
List<String> allAssetOnDatabase =
List<String> allAssetsInDatabase =
await _backupService.getDeviceBackupAsset();
// Find asset that were backup from selected albums
Set<String> selectedAlbumsBackupAssets =
Set.from(allUniqueAssets.map((e) => e.id));
selectedAlbumsBackupAssets
.removeWhere((assetId) => !allAssetOnDatabase.contains(assetId));
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
if (allUniqueAssets.isEmpty) {
debugPrint("No Asset On Device");
state = state.copyWith(
backupProgress: BackUpProgressEnum.idle,
allAssetOnDatabase: allAssetOnDatabase,
allAssetsInDatabase: allAssetsInDatabase,
allUniqueAssets: {},
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
);
return;
} else {
state = state.copyWith(
allAssetOnDatabase: allAssetOnDatabase,
allAssetsInDatabase: allAssetsInDatabase,
allUniqueAssets: allUniqueAssets,
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
);
@@ -222,6 +225,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
// Save to persistent storage
_updatePersistentAlbumsSelection();
return;
}
///
@@ -229,10 +234,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// which albums are selected or excluded
/// and then update the UI according to those information
///
void getBackupInfo() async {
await getBackupAlbumsInfo();
_updateServerInfo();
_updateBackupAssetCount();
Future<void> getBackupInfo() async {
await _getBackupAlbumsInfo();
await _updateServerInfo();
await _updateBackupAssetCount();
}
///
@@ -255,11 +260,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// Invoke backup process
///
void startBackupProcess() async {
_updateServerInfo();
_updateBackupAssetCount();
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
await getBackupInfo();
var authResult = await PhotoManager.requestPermissionExtend();
if (authResult.isAuth) {
await PhotoManager.clearFileCache();
@@ -270,10 +274,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
return;
}
Set<AssetEntity> assetsWillBeBackup = state.allUniqueAssets;
Set<AssetEntity> assetsWillBeBackup = Set.from(state.allUniqueAssets);
// Remove item that has already been backed up
for (var assetId in state.allAssetOnDatabase) {
for (var assetId in state.allAssetsInDatabase) {
assetsWillBeBackup.removeWhere((e) => e.id == assetId);
}
@@ -300,8 +304,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state = state.copyWith(selectedAlbumsBackupAssetsIds: {
...state.selectedAlbumsBackupAssetsIds,
deviceAssetId
}, allAssetOnDatabase: [
...state.allAssetOnDatabase,
}, allAssetsInDatabase: [
...state.allAssetsInDatabase,
deviceAssetId
]);
@@ -320,7 +324,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
}
void _updateServerInfo() async {
Future<void> _updateServerInfo() async {
var serverInfo = await _serverInfoService.getServerInfo();
// Update server info
@@ -338,21 +342,18 @@ class BackupNotifier extends StateNotifier<BackUpState> {
}
void resumeBackup() {
var authState = ref?.read(authenticationProvider);
// Check if user is login
var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
// User has been logged out return
if (authState != null) {
if (accessKey == null || !authState.isAuthenticated) {
if (accessKey == null || !_authState.isAuthenticated) {
debugPrint("[resumeBackup] not authenticated - abort");
return;
}
// Check if this device is enable backup by the user
if ((authState.deviceInfo.deviceId == authState.deviceId) &&
authState.deviceInfo.isAutoBackup) {
if ((_authState.deviceInfo.deviceId == _authState.deviceId) &&
_authState.deviceInfo.isAutoBackup) {
// check if backup is alreayd in process - then return
if (state.backupProgress == BackUpProgressEnum.inProgress) {
debugPrint("[resumeBackup] Backup is already in progress - abort");
@@ -367,9 +368,12 @@ class BackupNotifier extends StateNotifier<BackUpState> {
return;
}
}
}
final backupProvider =
StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
return BackupNotifier(ref: ref);
return BackupNotifier(
ref.watch(backupServiceProvider),
ref.watch(serverInfoServiceProvider),
ref.watch(authenticationProvider),
);
});

View File

@@ -5,6 +5,7 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/services/network.service.dart';
import 'package:immich_mobile/shared/models/device_info.model.dart';
@@ -14,20 +15,28 @@ import 'package:http_parser/http_parser.dart';
import 'package:path/path.dart' as p;
import 'package:cancellation_token_http/http.dart' as http;
final backupServiceProvider =
Provider((ref) => BackupService(ref.watch(networkServiceProvider)));
class BackupService {
final NetworkService _networkService = NetworkService();
final NetworkService _networkService;
BackupService(this._networkService);
Future<List<String>> getDeviceBackupAsset() async {
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
Response response = await _networkService.getRequest(url: "asset/$deviceId");
Response response =
await _networkService.getRequest(url: "asset/$deviceId");
List<dynamic> result = jsonDecode(response.toString());
return result.cast<String>();
}
backupAsset(Set<AssetEntity> assetList, http.CancellationToken cancelToken,
Function(String, String) singleAssetDoneCb, Function(int, int) uploadProgress) async {
backupAsset(
Set<AssetEntity> assetList,
http.CancellationToken cancelToken,
Function(String, String) singleAssetDoneCb,
Function(int, int) uploadProgress) async {
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
File? file;
@@ -44,7 +53,8 @@ class BackupService {
if (file != null) {
String originalFileName = await entity.titleAsync;
String fileNameWithoutPath = originalFileName.toString().split(".")[0];
String fileNameWithoutPath =
originalFileName.toString().split(".")[0];
var fileExtension = p.extension(file.path);
var mimeType = FileHelper.getMimeType(file.path);
var fileStream = file.openRead();
@@ -59,24 +69,12 @@ class BackupService {
),
);
// Build thumbnail multipart data
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(1440, 2560));
if (thumbnailData != null) {
thumbnailUploadData = http.MultipartFile.fromBytes(
"thumbnailData",
List.from(thumbnailData),
filename: fileNameWithoutPath,
contentType: MediaType(
"image",
"jpeg",
),
);
}
var box = Hive.box(userInfoBox);
var req = MultipartRequest('POST', Uri.parse('$savedEndpoint/asset/upload'),
onProgress: ((bytes, totalBytes) => uploadProgress(bytes, totalBytes)));
var req = MultipartRequest(
'POST', Uri.parse('$savedEndpoint/asset/upload'),
onProgress: ((bytes, totalBytes) =>
uploadProgress(bytes, totalBytes)));
req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}";
req.fields['deviceAssetId'] = entity.id;
@@ -88,9 +86,6 @@ class BackupService {
req.fields['fileExtension'] = fileExtension;
req.fields['duration'] = entity.videoDuration.toString();
if (thumbnailUploadData != null) {
req.files.add(thumbnailUploadData);
}
req.files.add(assetRawUploadData);
var res = await req.send(cancellationToken: cancelToken);
@@ -126,7 +121,8 @@ class BackupService {
}
}
Future<DeviceInfoRemote> setAutoBackup(bool status, String deviceId, String deviceType) async {
Future<DeviceInfoRemote> setAutoBackup(
bool status, String deviceId, String deviceType) async {
var res = await _networkService.patchRequest(url: 'device-info', data: {
"isAutoBackup": status,
"deviceId": deviceId,

View File

@@ -14,16 +14,22 @@ class AlbumInfoCard extends HookConsumerWidget {
final Uint8List? imageData;
final AssetPathEntity albumInfo;
const AlbumInfoCard({Key? key, this.imageData, required this.albumInfo}) : super(key: key);
const AlbumInfoCard({Key? key, this.imageData, required this.albumInfo})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
final bool isSelected =
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
ColorFilter selectedFilter = ColorFilter.mode(Theme.of(context).primaryColor.withAlpha(100), BlendMode.darken);
ColorFilter excludedFilter = ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
ColorFilter unselectedFilter = const ColorFilter.mode(Colors.black, BlendMode.color);
ColorFilter selectedFilter = ColorFilter.mode(
Theme.of(context).primaryColor.withAlpha(100), BlendMode.darken);
ColorFilter excludedFilter =
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
ColorFilter unselectedFilter =
const ColorFilter.mode(Colors.black, BlendMode.color);
_buildSelectedTextBox() {
if (isSelected) {
@@ -32,7 +38,8 @@ class AlbumInfoCard extends HookConsumerWidget {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
label: const Text(
"INCLUDED",
style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
),
backgroundColor: Theme.of(context).primaryColor,
);
@@ -42,13 +49,14 @@ class AlbumInfoCard extends HookConsumerWidget {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
label: const Text(
"EXCLUDED",
style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
),
backgroundColor: Colors.red[300],
);
}
return Container();
return const SizedBox();
}
_buildImageFilter() {
@@ -85,10 +93,15 @@ class AlbumInfoCard extends HookConsumerWidget {
HapticFeedback.selectionClick();
if (isExcluded) {
ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(albumInfo);
ref
.watch(backupProvider.notifier)
.removeExcludedAlbumForBackup(albumInfo);
} else {
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo)) {
ref
.watch(backupProvider)
.selectedBackupAlbums
.contains(albumInfo)) {
ImmichToast.show(
context: context,
msg: "Cannot exclude the only album",
@@ -98,7 +111,9 @@ class AlbumInfoCard extends HookConsumerWidget {
return;
}
ref.watch(backupProvider.notifier).addExcludedAlbumForBackup(albumInfo);
ref
.watch(backupProvider.notifier)
.addExcludedAlbumForBackup(albumInfo);
}
},
child: Card(
@@ -121,18 +136,26 @@ class AlbumInfoCard extends HookConsumerWidget {
width: 200,
height: 200,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12)),
image: DecorationImage(
colorFilter: _buildImageFilter(),
image: imageData != null
? MemoryImage(imageData!)
: const AssetImage('assets/immich-logo-no-outline.png') as ImageProvider,
: const AssetImage(
'assets/immich-logo-no-outline.png')
as ImageProvider,
fit: BoxFit.cover,
),
),
child: null,
),
Positioned(bottom: 10, left: 25, child: _buildSelectedTextBox())
Positioned(
bottom: 10,
left: 25,
child: _buildSelectedTextBox(),
)
],
),
Padding(
@@ -150,13 +173,16 @@ class AlbumInfoCard extends HookConsumerWidget {
Text(
albumInfo.name,
style: TextStyle(
fontSize: 14, color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
fontSize: 14,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold),
),
Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Text(
albumInfo.assetCount.toString() + (albumInfo.isAll ? " (ALL)" : ""),
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
'${albumInfo.assetCount} ${(albumInfo.isAll ? " (ALL)" : "")}',
style: TextStyle(
fontSize: 12, color: Colors.grey[600]),
),
)
],
@@ -165,7 +191,8 @@ class AlbumInfoCard extends HookConsumerWidget {
),
IconButton(
onPressed: () {
AutoRouter.of(context).push(AlbumPreviewRoute(album: albumInfo));
AutoRouter.of(context)
.push(AlbumPreviewRoute(album: albumInfo));
},
icon: Icon(
Icons.image_outlined,

View File

@@ -4,7 +4,12 @@ class BackupInfoCard extends StatelessWidget {
final String title;
final String subtitle;
final String info;
const BackupInfoCard({Key? key, required this.title, required this.subtitle, required this.info}) : super(key: key);
const BackupInfoCard(
{Key? key,
required this.title,
required this.subtitle,
required this.info})
: super(key: key);
@override
Widget build(BuildContext context) {

View File

@@ -16,7 +16,8 @@ class AlbumPreviewPage extends HookConsumerWidget {
final assets = useState<List<AssetEntity>>([]);
_getAssetsInAlbum() async {
assets.value = await album.getAssetListRange(start: 0, end: album.assetCount);
assets.value =
await album.getAssetListRange(start: 0, end: album.assetCount);
}
useEffect(() {
@@ -37,7 +38,10 @@ class AlbumPreviewPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 4.0),
child: Text(
"ID ${album.id}",
style: TextStyle(fontSize: 10, color: Colors.grey[600], fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
fontWeight: FontWeight.bold),
),
),
],
@@ -55,8 +59,9 @@ class AlbumPreviewPage extends HookConsumerWidget {
),
itemCount: assets.value.length,
itemBuilder: (context, index) {
Future<Uint8List?> thumbData =
assets.value[index].thumbnailDataWithSize(const ThumbnailSize(200, 200), quality: 50);
Future<Uint8List?> thumbData = assets.value[index]
.thumbnailDataWithSize(const ThumbnailSize(200, 200),
quality: 50);
return FutureBuilder<Uint8List?>(
future: thumbData,

View File

@@ -17,7 +17,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
useEffect(() {
ref.read(backupProvider.notifier).getBackupAlbumsInfo();
ref.read(backupProvider.notifier).getBackupInfo();
return null;
}, []);
@@ -37,8 +37,12 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
itemBuilder: ((context, index) {
var thumbnailData = availableAlbums[index].thumbnailData;
return Padding(
padding: index == 0 ? const EdgeInsets.only(left: 16.00) : const EdgeInsets.all(0),
child: AlbumInfoCard(imageData: thumbnailData, albumInfo: availableAlbums[index].albumEntity),
padding: index == 0
? const EdgeInsets.only(left: 16.00)
: const EdgeInsets.all(0),
child: AlbumInfoCard(
imageData: thumbnailData,
albumInfo: availableAlbums[index].albumEntity),
);
}),
),
@@ -67,10 +71,14 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
onTap: removeSelection,
child: Chip(
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
label: Text(
album.name,
style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.bold),
),
backgroundColor: Theme.of(context).primaryColor,
deleteIconColor: Colors.white,
@@ -88,7 +96,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
_buildExcludedAlbumNameChip() {
return excludedBackupAlbums.map((album) {
void removeSelection() {
ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(album);
ref
.watch(backupProvider.notifier)
.removeExcludedAlbumForBackup(album);
}
return GestureDetector(
@@ -97,10 +107,14 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
padding: const EdgeInsets.only(right: 8.0),
child: Chip(
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
label: Text(
album.name,
style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.bold),
),
backgroundColor: Colors.red[300],
deleteIconColor: Colors.white,
@@ -142,7 +156,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap(
children: [..._buildSelectedAlbumNameChip(), ..._buildExcludedAlbumNameChip()],
children: [
..._buildSelectedAlbumNameChip(),
..._buildExcludedAlbumNameChip()
],
),
),
@@ -165,10 +182,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
visualDensity: VisualDensity.compact,
title: Text(
"Total unique assets",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.grey[700]),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.grey[700]),
),
trailing: Text(
ref.watch(backupProvider).allUniqueAssets.length.toString(),
'${ref.watch(backupProvider).allUniqueAssets.length}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
@@ -179,7 +199,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
ListTile(
title: Text(
"Albums on device (${availableAlbums.length.toString()})",
"Albums on device (${availableAlbums.length})",
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
subtitle: Padding(
@@ -206,7 +226,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
elevation: 5,
title: Text(
'Selection Info',
@@ -221,7 +242,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
children: [
Text(
'Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.',
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
style: TextStyle(
fontSize: 14, color: Colors.grey[700]),
),
],
),

View File

@@ -26,7 +26,7 @@ class BackupControllerPage extends HookConsumerWidget {
useEffect(() {
if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
ref.read(backupProvider.notifier).getBackupInfo();
ref.watch(backupProvider.notifier).getBackupInfo();
}
ref
@@ -96,12 +96,11 @@ class BackupControllerPage extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
!isAutoBackup
? const Text(
if (!isAutoBackup)
const Text(
"Turn on backup to automatically upload new assets to the server.",
style: TextStyle(fontSize: 14),
)
: Container(),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: OutlinedButton(
@@ -112,13 +111,15 @@ class BackupControllerPage extends HookConsumerWidget {
),
),
onPressed: () {
isAutoBackup
? ref
.watch(authenticationProvider.notifier)
.setAutoBackup(false)
: ref
.watch(authenticationProvider.notifier)
if (isAutoBackup) {
ref
.read(authenticationProvider.notifier)
.setAutoBackup(false);
} else {
ref
.read(authenticationProvider.notifier)
.setAutoBackup(true);
}
},
child: Text("Turn $backupBtnText Backup",
style: const TextStyle(fontWeight: FontWeight.bold)),
@@ -187,7 +188,7 @@ class BackupControllerPage extends HookConsumerWidget {
),
);
} else {
return Container();
return const SizedBox();
}
}
@@ -212,7 +213,7 @@ class BackupControllerPage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Albums to be backup",
"Albums to be backed up",
style: TextStyle(color: Color(0xFF808080), fontSize: 12),
),
_buildSelectedAlbumName(),
@@ -282,14 +283,12 @@ class BackupControllerPage extends HookConsumerWidget {
),
BackupInfoCard(
title: "Backup",
subtitle:
"Photos and videos from selected albums that are backup",
subtitle: "Backed up photos and videos",
info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
),
BackupInfoCard(
title: "Remainder",
subtitle:
"Photos and videos that has not been backing up from selected albums",
subtitle: "Remaining photos and albums to back up from selection",
info:
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
),

View File

@@ -35,7 +35,8 @@ class DeleteAssetResponse {
String toJson() => json.encode(toMap());
factory DeleteAssetResponse.fromJson(String source) => DeleteAssetResponse.fromMap(json.decode(source));
factory DeleteAssetResponse.fromJson(String source) =>
DeleteAssetResponse.fromMap(json.decode(source));
@override
String toString() => 'DeleteAssetResponse(id: $id, status: $status)';
@@ -44,7 +45,9 @@ class DeleteAssetResponse {
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DeleteAssetResponse && other.id == id && other.status == status;
return other is DeleteAssetResponse &&
other.id == id &&
other.status == status;
}
@override

View File

@@ -31,13 +31,15 @@ class ImmichAssetGroupByDate {
factory ImmichAssetGroupByDate.fromMap(Map<String, dynamic> map) {
return ImmichAssetGroupByDate(
date: map['date'] ?? '',
assets: List<ImmichAsset>.from(map['assets']?.map((x) => ImmichAsset.fromMap(x))),
assets: List<ImmichAsset>.from(
map['assets']?.map((x) => ImmichAsset.fromMap(x))),
);
}
String toJson() => json.encode(toMap());
factory ImmichAssetGroupByDate.fromJson(String source) => ImmichAssetGroupByDate.fromMap(json.decode(source));
factory ImmichAssetGroupByDate.fromJson(String source) =>
ImmichAssetGroupByDate.fromMap(json.decode(source));
@override
String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)';
@@ -46,7 +48,9 @@ class ImmichAssetGroupByDate {
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ImmichAssetGroupByDate && other.date == date && listEquals(other.assets, assets);
return other is ImmichAssetGroupByDate &&
other.date == date &&
listEquals(other.assets, assets);
}
@override
@@ -86,17 +90,20 @@ class GetAllAssetResponse {
factory GetAllAssetResponse.fromMap(Map<String, dynamic> map) {
return GetAllAssetResponse(
count: map['count']?.toInt() ?? 0,
data: List<ImmichAssetGroupByDate>.from(map['data']?.map((x) => ImmichAssetGroupByDate.fromMap(x))),
data: List<ImmichAssetGroupByDate>.from(
map['data']?.map((x) => ImmichAssetGroupByDate.fromMap(x))),
nextPageKey: map['nextPageKey'] ?? '',
);
}
String toJson() => json.encode(toMap());
factory GetAllAssetResponse.fromJson(String source) => GetAllAssetResponse.fromMap(json.decode(source));
factory GetAllAssetResponse.fromJson(String source) =>
GetAllAssetResponse.fromMap(json.decode(source));
@override
String toString() => 'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';
String toString() =>
'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';
@override
bool operator ==(Object other) {

View File

@@ -37,14 +37,16 @@ class HomePageState {
factory HomePageState.fromMap(Map<String, dynamic> map) {
return HomePageState(
isMultiSelectEnable: map['isMultiSelectEnable'] ?? false,
selectedItems: Set<ImmichAsset>.from(map['selectedItems']?.map((x) => ImmichAsset.fromMap(x))),
selectedItems: Set<ImmichAsset>.from(
map['selectedItems']?.map((x) => ImmichAsset.fromMap(x))),
selectedDateGroup: Set<String>.from(map['selectedDateGroup']),
);
}
String toJson() => json.encode(toMap());
factory HomePageState.fromJson(String source) => HomePageState.fromMap(json.decode(source));
factory HomePageState.fromJson(String source) =>
HomePageState.fromMap(json.decode(source));
@override
String toString() =>
@@ -62,5 +64,8 @@ class HomePageState {
}
@override
int get hashCode => isMultiSelectEnable.hashCode ^ selectedItems.hashCode ^ selectedDateGroup.hashCode;
int get hashCode =>
isMultiSelectEnable.hashCode ^
selectedItems.hashCode ^
selectedDateGroup.hashCode;
}

View File

@@ -13,7 +13,8 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
);
void addSelectedDateGroup(String dateGroupTitle) {
state = state.copyWith(selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle});
state = state.copyWith(
selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle});
}
void removeSelectedDateGroup(String dateGroupTitle) {
@@ -25,11 +26,13 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
}
void enableMultiSelect(Set<ImmichAsset> selectedItems) {
state = state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
state =
state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
}
void disableMultiSelect() {
state = state.copyWith(isMultiSelectEnable: false, selectedItems: {}, selectedDateGroup: {});
state = state.copyWith(
isMultiSelectEnable: false, selectedItems: {}, selectedDateGroup: {});
}
void addSingleSelectedItem(ImmichAsset asset) {
@@ -60,4 +63,5 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
}
final homePageStateProvider =
StateNotifierProvider<HomePageStateNotifier, HomePageState>(((ref) => HomePageStateNotifier()));
StateNotifierProvider<HomePageStateNotifier, HomePageState>(
((ref) => HomePageStateNotifier()));

View File

@@ -50,37 +50,46 @@ class UploadProfileImageState {
String toJson() => json.encode(toMap());
factory UploadProfileImageState.fromJson(String source) => UploadProfileImageState.fromMap(json.decode(source));
factory UploadProfileImageState.fromJson(String source) =>
UploadProfileImageState.fromMap(json.decode(source));
@override
String toString() => 'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)';
String toString() =>
'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UploadProfileImageState && other.status == status && other.profileImagePath == profileImagePath;
return other is UploadProfileImageState &&
other.status == status &&
other.profileImagePath == profileImagePath;
}
@override
int get hashCode => status.hashCode ^ profileImagePath.hashCode;
}
class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState> {
UploadProfileImageNotifier()
class UploadProfileImageNotifier
extends StateNotifier<UploadProfileImageState> {
UploadProfileImageNotifier(this._userSErvice)
: super(UploadProfileImageState(
profileImagePath: '',
status: UploadProfileStatus.idle,
));
final UserService _userSErvice;
Future<bool> upload(XFile file) async {
state = state.copyWith(status: UploadProfileStatus.loading);
var res = await UserService().uploadProfileImage(file);
var res = await _userSErvice.uploadProfileImage(file);
if (res != null) {
debugPrint("Succesfully upload profile image");
state = state.copyWith(status: UploadProfileStatus.success, profileImagePath: res.profileImagePath);
state = state.copyWith(
status: UploadProfileStatus.success,
profileImagePath: res.profileImagePath);
return true;
}
@@ -90,4 +99,5 @@ class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState>
}
final uploadProfileImageProvider =
StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>(((ref) => UploadProfileImageNotifier()));
StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>(
((ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider))));

View File

@@ -1,21 +1,27 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/models/delete_asset_response.model.dart';
import 'package:immich_mobile/modules/home/models/get_all_asset_response.model.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
import 'package:immich_mobile/shared/services/network.service.dart';
final assetServiceProvider =
Provider((ref) => AssetService(ref.watch(networkServiceProvider)));
class AssetService {
final NetworkService _networkService = NetworkService();
final NetworkService _networkService;
AssetService(this._networkService);
Future<List<ImmichAsset>?> getAllAsset() async {
var res = await _networkService.getRequest(url: "asset/");
try {
List<dynamic> decodedData = jsonDecode(res.toString());
List<ImmichAsset> result = List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
List<ImmichAsset> result =
List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
return result;
} catch (e) {
debugPrint("Error getAllAsset ${e.toString()}");
@@ -62,7 +68,8 @@ class AssetService {
List<dynamic> decodedData = jsonDecode(res.toString());
List<ImmichAsset> result = List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
List<ImmichAsset> result =
List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
if (result.isNotEmpty) {
return result;
}
@@ -90,7 +97,8 @@ class AssetService {
}
}
Future<List<DeleteAssetResponse>?> deleteAssets(Set<ImmichAsset> deleteAssets) async {
Future<List<DeleteAssetResponse>?> deleteAssets(
Set<ImmichAsset> deleteAssets) async {
try {
var payload = [];
@@ -98,11 +106,13 @@ class AssetService {
payload.add(asset.id);
}
var res = await _networkService.deleteRequest(url: "asset/", data: {"ids": payload});
var res = await _networkService
.deleteRequest(url: "asset/", data: {"ids": payload});
List<dynamic> decodedData = jsonDecode(res.toString());
List<DeleteAssetResponse> result = List.from(decodedData.map((a) => DeleteAssetResponse.fromMap(a)));
List<DeleteAssetResponse> result =
List.from(decodedData.map((a) => DeleteAssetResponse.fromMap(a)));
return result;
} catch (e) {

View File

@@ -13,7 +13,8 @@ class ControlBottomAppBar extends StatelessWidget {
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height * 0.15,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(15), topRight: Radius.circular(15)),
color: Colors.grey[300]?.withOpacity(0.98),
),
child: Column(
@@ -46,7 +47,11 @@ class ControlBottomAppBar extends StatelessWidget {
}
class ControlBoxButton extends StatelessWidget {
const ControlBoxButton({Key? key, required this.label, required this.iconData, required this.onPressed})
const ControlBoxButton(
{Key? key,
required this.label,
required this.iconData,
required this.onPressed})
: super(key: key);
final String label;

View File

@@ -18,9 +18,12 @@ class DailyTitleText extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
var currentYear = DateTime.now().year;
var groupYear = DateTime.parse(isoDate).year;
var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
var formatDateTemplate =
currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
var dateText =
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable;
var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
var selectedItems = ref.watch(homePageStateProvider).selectedItems;
@@ -35,23 +38,42 @@ class DailyTitleText extends ConsumerWidget {
selectedDateGroup.contains(dateText) &&
selectedItems.length != assetGroup.length) {
// Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items
ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText);
ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup);
} else if (isMultiSelectEnable && selectedDateGroup.contains(dateText) && selectedDateGroup.length > 1) {
ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText);
ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup);
ref
.watch(homePageStateProvider.notifier)
.removeSelectedDateGroup(dateText);
ref
.watch(homePageStateProvider.notifier)
.removeMultipleSelectedItem(assetGroup);
} else if (isMultiSelectEnable &&
selectedDateGroup.contains(dateText) &&
selectedDateGroup.length > 1) {
ref
.watch(homePageStateProvider.notifier)
.removeSelectedDateGroup(dateText);
ref
.watch(homePageStateProvider.notifier)
.removeMultipleSelectedItem(assetGroup);
} else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText);
ref.watch(homePageStateProvider.notifier).addMultipleSelectedItems(assetGroup);
ref
.watch(homePageStateProvider.notifier)
.addSelectedDateGroup(dateText);
ref
.watch(homePageStateProvider.notifier)
.addMultipleSelectedItems(assetGroup);
} else {
ref.watch(homePageStateProvider.notifier).enableMultiSelect(assetGroup.toSet());
ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText);
ref
.watch(homePageStateProvider.notifier)
.enableMultiSelect(assetGroup.toSet());
ref
.watch(homePageStateProvider.notifier)
.addSelectedDateGroup(dateText);
}
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 29.0, bottom: 29.0, left: 12.0, right: 12.0),
padding: const EdgeInsets.only(
top: 29.0, bottom: 29.0, left: 12.0, right: 12.0),
child: Row(
children: [
Text(

View File

@@ -14,7 +14,8 @@ class DeleteDialog extends ConsumerWidget {
backgroundColor: Colors.grey[200],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
title: const Text("Delete Permanently"),
content: const Text("These items will be permanently deleted from Immich and from your device"),
content: const Text(
"These items will be permanently deleted from Immich and from your device"),
actions: [
TextButton(
onPressed: () {
@@ -27,7 +28,9 @@ class DeleteDialog extends ConsumerWidget {
),
TextButton(
onPressed: () {
ref.watch(assetProvider.notifier).deleteAssets(homePageState.selectedItems);
ref
.watch(assetProvider.notifier)
.deleteAssets(homePageState.selectedItems);
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
Navigator.of(context).pop();

View File

@@ -34,7 +34,7 @@ class DisableMultiSelectButton extends ConsumerWidget {
},
icon: const Icon(Icons.close_rounded),
label: Text(
selectedItemCount.toString(),
'$selectedItemCount',
style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 18),
)),

View File

@@ -615,7 +615,7 @@ class SlideFadeTransition extends StatelessWidget {
return AnimatedBuilder(
animation: animation,
builder: (context, child) =>
animation.value == 0.0 ? Container() : child!,
animation.value == 0.0 ? const SizedBox() : child!,
child: SlideTransition(
position: Tween(
begin: const Offset(0.3, 0.0),

View File

@@ -25,9 +25,8 @@ class ImageGrid extends ConsumerWidget {
child: Stack(
children: [
ThumbnailImage(asset: assetGroup[index]),
assetType == 'IMAGE'
? Container()
: Positioned(
if (assetType != 'IMAGE')
Positioned(
top: 5,
right: 5,
child: Row(
@@ -45,7 +44,7 @@ class ImageGrid extends ConsumerWidget {
),
],
),
)
),
],
),
);

View File

@@ -49,8 +49,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
},
),
),
serverInfoState.isVersionMismatch
? Positioned(
if (serverInfoState.isVersionMismatch)
Positioned(
bottom: 12,
right: 12,
child: GestureDetector(
@@ -71,8 +71,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
),
),
),
)
: Container(),
),
],
);
},
@@ -90,8 +89,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
Stack(
alignment: AlignmentDirectional.center,
children: [
backupState.backupProgress == BackUpProgressEnum.inProgress
? Positioned(
if (backupState.backupProgress == BackUpProgressEnum.inProgress)
Positioned(
top: 10,
right: 12,
child: SizedBox(
@@ -103,8 +102,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
Theme.of(context).primaryColor),
),
),
)
: Container(),
),
IconButton(
splashRadius: 25,
iconSize: 30,
@@ -129,18 +127,15 @@ class ImmichSliverAppBar extends ConsumerWidget {
}
},
),
backupState.backupProgress == BackUpProgressEnum.inProgress
? Positioned(
if (backupState.backupProgress == BackUpProgressEnum.inProgress)
Positioned(
bottom: 5,
child: Text(
(backupState.allUniqueAssets.length -
backupState.selectedAlbumsBackupAssetsIds.length)
.toString(),
style: const TextStyle(
fontSize: 9, fontWeight: FontWeight.bold),
'${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}',
style:
const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
),
),
)
: Container()
],
),
],

View File

@@ -87,7 +87,7 @@ class ProfileDrawer extends HookConsumerWidget {
return const ImmichLoadingIndicator();
}
return Container();
return const SizedBox();
}
_pickUserProfileImage() async {
@@ -191,7 +191,7 @@ class ProfileDrawer extends HookConsumerWidget {
),
onTap: () async {
bool res =
await ref.read(authenticationProvider.notifier).logout();
await ref.watch(authenticationProvider.notifier).logout();
if (res) {
ref.watch(backupProvider.notifier).cancelBackup();

View File

@@ -25,7 +25,8 @@ class ThumbnailImage extends HookConsumerWidget {
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable;
var deviceId = ref.watch(authenticationProvider).deviceId;
Widget _buildSelectionIcon(ImmichAsset asset) {
@@ -45,12 +46,20 @@ class ThumbnailImage extends HookConsumerWidget {
return GestureDetector(
onTap: () {
debugPrint("View ${asset.id}");
if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length == 1) {
if (isMultiSelectEnable &&
selectedAsset.contains(asset) &&
selectedAsset.length == 1) {
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
} else if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length > 1) {
ref.watch(homePageStateProvider.notifier).removeSingleSelectedItem(asset);
} else if (isMultiSelectEnable &&
selectedAsset.contains(asset) &&
selectedAsset.length > 1) {
ref
.watch(homePageStateProvider.notifier)
.removeSingleSelectedItem(asset);
} else if (isMultiSelectEnable && !selectedAsset.contains(asset)) {
ref.watch(homePageStateProvider.notifier).addSingleSelectedItem(asset);
ref
.watch(homePageStateProvider.notifier)
.addSingleSelectedItem(asset);
} else {
if (asset.type == 'IMAGE') {
AutoRouter.of(context).push(
@@ -65,7 +74,8 @@ class ThumbnailImage extends HookConsumerWidget {
} else {
AutoRouter.of(context).push(
VideoViewerRoute(
videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
videoUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
asset: asset),
);
}
@@ -83,7 +93,8 @@ class ThumbnailImage extends HookConsumerWidget {
Container(
decoration: BoxDecoration(
border: isMultiSelectEnable && selectedAsset.contains(asset)
? Border.all(color: Theme.of(context).primaryColorLight, width: 10)
? Border.all(
color: Theme.of(context).primaryColorLight, width: 10)
: const Border(),
),
child: CachedNetworkImage(
@@ -93,11 +104,15 @@ class ThumbnailImage extends HookConsumerWidget {
memCacheHeight: asset.type == 'IMAGE' ? 250 : 400,
fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
httpHeaders: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
progressIndicatorBuilder: (context, url, downloadProgress) =>
Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress),
child: CircularProgressIndicator(
value: downloadProgress.progress),
),
errorWidget: (context, url, error) {
return Icon(
@@ -107,22 +122,21 @@ class ThumbnailImage extends HookConsumerWidget {
},
),
),
Container(
child: isMultiSelectEnable
? Padding(
if (isMultiSelectEnable)
Padding(
padding: const EdgeInsets.all(3.0),
child: Align(
alignment: Alignment.topLeft,
child: _buildSelectionIcon(asset),
),
)
: Container(),
),
Positioned(
right: 10,
bottom: 5,
child: Icon(
(deviceId != asset.deviceId) ? Icons.cloud_done_outlined : Icons.photo_library_rounded,
(deviceId != asset.deviceId)
? Icons.cloud_done_outlined
: Icons.photo_library_rounded,
color: Colors.white,
size: 18,
),

View File

@@ -38,17 +38,10 @@ class HomePage extends HookConsumerWidget {
}
_buildSelectedItemCountIndicator() {
return isMultiSelectEnable
? DisableMultiSelectButton(
onPressed:
ref.watch(homePageStateProvider.notifier).disableMultiSelect,
return DisableMultiSelectButton(
onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
selectedItemCount: homePageState.selectedItems.length,
)
: Container();
}
_buildBottomAppBar() {
return isMultiSelectEnable ? const ControlBottomAppBar() : Container();
);
}
Widget _buildBody() {
@@ -121,8 +114,10 @@ class HomePage extends HookConsumerWidget {
),
),
),
if (isMultiSelectEnable) ...[
_buildSelectedItemCountIndicator(),
_buildBottomAppBar(),
const ControlBottomAppBar(),
],
],
),
);

View File

@@ -11,7 +11,7 @@ class AuthenticationState {
final String firstName;
final String lastName;
final bool isAdmin;
final bool isFirstLogin;
final bool shouldChangePassword;
final String profileImagePath;
final DeviceInfoRemote deviceInfo;
@@ -24,7 +24,7 @@ class AuthenticationState {
required this.firstName,
required this.lastName,
required this.isAdmin,
required this.isFirstLogin,
required this.shouldChangePassword,
required this.profileImagePath,
required this.deviceInfo,
});
@@ -38,7 +38,7 @@ class AuthenticationState {
String? firstName,
String? lastName,
bool? isAdmin,
bool? isFirstLoggedIn,
bool? shouldChangePassword,
String? profileImagePath,
DeviceInfoRemote? deviceInfo,
}) {
@@ -51,17 +51,12 @@ class AuthenticationState {
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
isAdmin: isAdmin ?? this.isAdmin,
isFirstLogin: isFirstLoggedIn ?? isFirstLogin,
shouldChangePassword: shouldChangePassword ?? this.shouldChangePassword,
profileImagePath: profileImagePath ?? this.profileImagePath,
deviceInfo: deviceInfo ?? this.deviceInfo,
);
}
@override
String toString() {
return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, isFirstLoggedIn: $isFirstLogin, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)';
}
Map<String, dynamic> toMap() {
final result = <String, dynamic>{};
@@ -73,7 +68,7 @@ class AuthenticationState {
result.addAll({'firstName': firstName});
result.addAll({'lastName': lastName});
result.addAll({'isAdmin': isAdmin});
result.addAll({'isFirstLogin': isFirstLogin});
result.addAll({'shouldChangePassword': shouldChangePassword});
result.addAll({'profileImagePath': profileImagePath});
result.addAll({'deviceInfo': deviceInfo.toMap()});
@@ -90,7 +85,7 @@ class AuthenticationState {
firstName: map['firstName'] ?? '',
lastName: map['lastName'] ?? '',
isAdmin: map['isAdmin'] ?? false,
isFirstLogin: map['isFirstLogin'] ?? false,
shouldChangePassword: map['shouldChangePassword'] ?? false,
profileImagePath: map['profileImagePath'] ?? '',
deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']),
);
@@ -98,7 +93,13 @@ class AuthenticationState {
String toJson() => json.encode(toMap());
factory AuthenticationState.fromJson(String source) => AuthenticationState.fromMap(json.decode(source));
factory AuthenticationState.fromJson(String source) =>
AuthenticationState.fromMap(json.decode(source));
@override
String toString() {
return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)';
}
@override
bool operator ==(Object other) {
@@ -113,7 +114,7 @@ class AuthenticationState {
other.firstName == firstName &&
other.lastName == lastName &&
other.isAdmin == isAdmin &&
other.isFirstLogin == isFirstLogin &&
other.shouldChangePassword == shouldChangePassword &&
other.profileImagePath == profileImagePath &&
other.deviceInfo == deviceInfo;
}
@@ -128,7 +129,7 @@ class AuthenticationState {
firstName.hashCode ^
lastName.hashCode ^
isAdmin.hashCode ^
isFirstLogin.hashCode ^
shouldChangePassword.hashCode ^
profileImagePath.hashCode ^
deviceInfo.hashCode;
}

View File

@@ -16,5 +16,9 @@ class HiveSavedLoginInfo {
@HiveField(3)
bool isSaveLogin;
HiveSavedLoginInfo({required this.email, required this.password, required this.serverUrl, required this.isSaveLogin});
HiveSavedLoginInfo(
{required this.email,
required this.password,
required this.serverUrl,
required this.isSaveLogin});
}

View File

@@ -8,7 +8,7 @@ class LogInReponse {
final String lastName;
final String profileImagePath;
final bool isAdmin;
final bool isFirstLogin;
final bool shouldChangePassword;
LogInReponse({
required this.accessToken,
@@ -18,7 +18,7 @@ class LogInReponse {
required this.lastName,
required this.profileImagePath,
required this.isAdmin,
required this.isFirstLogin,
required this.shouldChangePassword,
});
LogInReponse copyWith({
@@ -29,7 +29,7 @@ class LogInReponse {
String? lastName,
String? profileImagePath,
bool? isAdmin,
bool? isFirstLogin,
bool? shouldChangePassword,
}) {
return LogInReponse(
accessToken: accessToken ?? this.accessToken,
@@ -39,7 +39,7 @@ class LogInReponse {
lastName: lastName ?? this.lastName,
profileImagePath: profileImagePath ?? this.profileImagePath,
isAdmin: isAdmin ?? this.isAdmin,
isFirstLogin: isFirstLogin ?? this.isFirstLogin,
shouldChangePassword: shouldChangePassword ?? this.shouldChangePassword,
);
}
@@ -53,7 +53,7 @@ class LogInReponse {
result.addAll({'lastName': lastName});
result.addAll({'profileImagePath': profileImagePath});
result.addAll({'isAdmin': isAdmin});
result.addAll({'isFirstLogin': isFirstLogin});
result.addAll({'shouldChangePassword': shouldChangePassword});
return result;
}
@@ -67,17 +67,18 @@ class LogInReponse {
lastName: map['lastName'] ?? '',
profileImagePath: map['profileImagePath'] ?? '',
isAdmin: map['isAdmin'] ?? false,
isFirstLogin: map['isFirstLogin'] ?? false,
shouldChangePassword: map['shouldChangePassword'] ?? false,
);
}
String toJson() => json.encode(toMap());
factory LogInReponse.fromJson(String source) => LogInReponse.fromMap(json.decode(source));
factory LogInReponse.fromJson(String source) =>
LogInReponse.fromMap(json.decode(source));
@override
String toString() {
return 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail, firstName: $firstName, lastName: $lastName, profileImagePath: $profileImagePath, isAdmin: $isAdmin, isFirstLogin: $isFirstLogin)';
return 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail, firstName: $firstName, lastName: $lastName, profileImagePath: $profileImagePath, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword)';
}
@override
@@ -92,7 +93,7 @@ class LogInReponse {
other.lastName == lastName &&
other.profileImagePath == profileImagePath &&
other.isAdmin == isAdmin &&
other.isFirstLogin == isFirstLogin;
other.shouldChangePassword == shouldChangePassword;
}
@override
@@ -104,6 +105,6 @@ class LogInReponse {
lastName.hashCode ^
profileImagePath.hashCode ^
isAdmin.hashCode ^
isFirstLogin.hashCode;
shouldChangePassword.hashCode;
}
}

View File

@@ -12,7 +12,8 @@ import 'package:immich_mobile/shared/services/network.service.dart';
import 'package:immich_mobile/shared/models/device_info.model.dart';
class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
AuthenticationNotifier(this.ref)
AuthenticationNotifier(
this._deviceInfoService, this._backupService, this._networkService)
: super(
AuthenticationState(
deviceId: "",
@@ -23,7 +24,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
lastName: '',
profileImagePath: '',
isAdmin: false,
isFirstLogin: false,
shouldChangePassword: false,
isAuthenticated: false,
deviceInfo: DeviceInfoRemote(
id: 0,
@@ -37,12 +38,12 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
),
);
final Ref ref;
final DeviceInfoService _deviceInfoService = DeviceInfoService();
final BackupService _backupService = BackupService();
final NetworkService _networkService = NetworkService();
final DeviceInfoService _deviceInfoService;
final BackupService _backupService;
final NetworkService _networkService;
Future<bool> login(String email, String password, String serverEndpoint, bool isSavedLoginInfo) async {
Future<bool> login(String email, String password, String serverEndpoint,
bool isSavedLoginInfo) async {
// Store server endpoint to Hive and test endpoint
if (serverEndpoint[serverEndpoint.length - 1] == "/") {
var validUrl = serverEndpoint.substring(0, serverEndpoint.length - 1);
@@ -71,7 +72,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
// Make sign-in request
try {
Response res = await _networkService.postRequest(url: 'auth/login', data: {'email': email, 'password': password});
Response res = await _networkService.postRequest(
url: 'auth/login', data: {'email': email, 'password': password});
var payload = LogInReponse.fromJson(res.toString());
@@ -85,7 +87,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
lastName: payload.lastName,
profileImagePath: payload.profileImagePath,
isAdmin: payload.isAdmin,
isFirstLoggedIn: payload.isFirstLogin,
shouldChangePassword: payload.shouldChangePassword,
);
if (isSavedLoginInfo) {
@@ -99,7 +101,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
serverUrl: Hive.box(userInfoBox).get(serverEndpointKey)),
);
} else {
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey);
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox)
.delete(savedLoginInfoKey);
}
} catch (e) {
return false;
@@ -107,8 +110,13 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
// Register device info
try {
Response res = await _networkService
.postRequest(url: 'device-info', data: {'deviceId': state.deviceId, 'deviceType': state.deviceType});
Response res = await _networkService.postRequest(
url: 'device-info',
data: {
'deviceId': state.deviceId,
'deviceType': state.deviceType,
},
);
DeviceInfoRemote deviceInfo = DeviceInfoRemote.fromJson(res.toString());
state = state.copyWith(deviceInfo: deviceInfo);
@@ -129,7 +137,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
firstName: '',
lastName: '',
profileImagePath: '',
isFirstLogin: false,
shouldChangePassword: false,
isAuthenticated: false,
isAdmin: false,
deviceInfo: DeviceInfoRemote(
@@ -151,15 +159,39 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
var deviceId = deviceInfo["deviceId"];
var deviceType = deviceInfo["deviceType"];
DeviceInfoRemote deviceInfoRemote = await _backupService.setAutoBackup(backupState, deviceId, deviceType);
DeviceInfoRemote deviceInfoRemote =
await _backupService.setAutoBackup(backupState, deviceId, deviceType);
state = state.copyWith(deviceInfo: deviceInfoRemote);
}
updateUserProfileImagePath(String path) {
state = state.copyWith(profileImagePath: path);
}
Future<bool> changePassword(String newPassword) async {
Response res = await _networkService.putRequest(
url: 'user',
data: {
'id': state.userId,
'password': newPassword,
'shouldChangePassword': false,
},
);
if (res.statusCode == 200) {
state = state.copyWith(shouldChangePassword: false);
return true;
} else {
return false;
}
}
}
final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
return AuthenticationNotifier(ref);
final authenticationProvider =
StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
return AuthenticationNotifier(
ref.watch(deviceInfoServiceProvider),
ref.watch(backupServiceProvider),
ref.watch(networkServiceProvider),
);
});

View File

@@ -0,0 +1,174 @@
import 'package:auto_route/auto_route.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/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
class ChangePasswordForm extends HookConsumerWidget {
const ChangePasswordForm({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final passwordController =
useTextEditingController.fromValue(TextEditingValue.empty);
final confirmPasswordController =
useTextEditingController.fromValue(TextEditingValue.empty);
final authState = ref.watch(authenticationProvider);
final formKey = GlobalKey<FormState>();
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: SingleChildScrollView(
child: Wrap(
spacing: 16,
runSpacing: 16,
alignment: WrapAlignment.start,
children: [
Text(
'Change Password',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Text(
'Hi ${authState.firstName} ${authState.lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.',
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
fontWeight: FontWeight.w600,
),
),
),
Form(
key: formKey,
child: Column(
children: [
PasswordInput(controller: passwordController),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ConfirmPasswordInput(
originalController: passwordController,
confirmController: confirmPasswordController,
),
),
ChangePasswordButton(
passwordController: passwordController,
formKey: formKey,
),
],
),
)
],
),
),
),
);
}
}
class PasswordInput extends StatelessWidget {
final TextEditingController controller;
const PasswordInput({Key? key, required this.controller}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
obscureText: true,
controller: controller,
decoration: const InputDecoration(
labelText: 'New Password',
border: OutlineInputBorder(),
hintText: 'New Password',
),
);
}
}
class ConfirmPasswordInput extends StatelessWidget {
final TextEditingController originalController;
final TextEditingController confirmController;
const ConfirmPasswordInput({
Key? key,
required this.originalController,
required this.confirmController,
}) : super(key: key);
String? _validateInput(String? email) {
if (confirmController.value != originalController.value) {
return 'Passwords do not match';
}
return null;
}
@override
Widget build(BuildContext context) {
return TextFormField(
obscureText: true,
controller: confirmController,
decoration: const InputDecoration(
labelText: 'Confirm Password',
hintText: 'Re-enter New Password',
border: OutlineInputBorder(),
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
);
}
}
class ChangePasswordButton extends ConsumerWidget {
final TextEditingController passwordController;
final GlobalKey<FormState> formKey;
const ChangePasswordButton({
Key? key,
required this.passwordController,
required this.formKey,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
visualDensity: VisualDensity.standard,
primary: Theme.of(context).primaryColor,
onPrimary: Colors.grey[50],
elevation: 2,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
),
onPressed: () async {
if (formKey.currentState!.validate()) {
var isSuccess = await ref
.watch(authenticationProvider.notifier)
.changePassword(passwordController.value.text);
if (isSuccess) {
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());
}
}
}
},
child: const Text(
"Change Password",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
));
}
}

View File

@@ -5,6 +5,7 @@ import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.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';
@@ -15,13 +16,17 @@ class LoginForm extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usernameController = useTextEditingController.fromValue(TextEditingValue.empty);
final passwordController = useTextEditingController.fromValue(TextEditingValue.empty);
final serverEndpointController = useTextEditingController(text: 'http://your-server-ip:2283');
final usernameController =
useTextEditingController.fromValue(TextEditingValue.empty);
final passwordController =
useTextEditingController.fromValue(TextEditingValue.empty);
final serverEndpointController =
useTextEditingController(text: 'http://your-server-ip:2283/api');
final isSaveLoginInfo = useState<bool>(false);
useEffect(() {
var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
var loginInfo =
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
if (loginInfo != null) {
usernameController.text = loginInfo.email;
@@ -64,11 +69,15 @@ class LoginForm extends HookConsumerWidget {
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
dense: true,
side: const BorderSide(color: Colors.grey, width: 1.5),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
enableFeedback: true,
title: const Text(
"Stay logged in",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey),
),
value: isSaveLoginInfo.value,
onChanged: (switchValue) {
@@ -94,12 +103,16 @@ class LoginForm extends HookConsumerWidget {
class ServerEndpointInput extends StatelessWidget {
final TextEditingController controller;
const ServerEndpointInput({Key? key, required this.controller}) : super(key: key);
const ServerEndpointInput({Key? key, required this.controller})
: super(key: key);
String? _validateInput(String? url) {
if (url == null) return null;
if (!url.startsWith(RegExp(r'https?://'))) return 'Please specify http:// or https://';
if (url?.startsWith(RegExp(r'https?://')) == true) {
return null;
} else {
return 'Please specify http:// or https://';
}
}
@override
@@ -107,7 +120,10 @@ class ServerEndpointInput extends StatelessWidget {
return TextFormField(
controller: controller,
decoration: const InputDecoration(
labelText: 'Server Endpoint URL', border: OutlineInputBorder(), hintText: 'http://your-server-ip:port'),
labelText: 'Server Endpoint URL',
border: OutlineInputBorder(),
hintText: 'http://your-server-ip:port',
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
);
@@ -131,8 +147,11 @@ class EmailInput extends StatelessWidget {
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
decoration:
const InputDecoration(labelText: 'Email', border: OutlineInputBorder(), hintText: 'youremail@email.com'),
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
hintText: 'youremail@email.com',
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
);
@@ -149,7 +168,10 @@ class PasswordInput extends StatelessWidget {
return TextFormField(
obscureText: true,
controller: controller,
decoration: const InputDecoration(labelText: 'Password', border: OutlineInputBorder(), hintText: 'password'),
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
hintText: 'password'),
);
}
}
@@ -183,17 +205,25 @@ class LoginButton extends ConsumerWidget {
ref.watch(assetProvider.notifier).clearAllAsset();
var isAuthenticated = await ref
.read(authenticationProvider.notifier)
.login(emailController.text, passwordController.text, serverEndpointController.text, isSavedLoginInfo);
.watch(authenticationProvider.notifier)
.login(emailController.text, passwordController.text,
serverEndpointController.text, isSavedLoginInfo);
if (isAuthenticated) {
// Resume backup (if enable) then navigate
if (ref.watch(authenticationProvider).shouldChangePassword &&
!ref.watch(authenticationProvider).isAdmin) {
AutoRouter.of(context).push(const ChangePasswordRoute());
} else {
ref.watch(backupProvider.notifier).resumeBackup();
AutoRouter.of(context).pushNamed("/tab-controller-page");
}
} else {
ImmichToast.show(
context: context,
msg: "Error logging you in, check server url, email and password!",
msg:
"Error logging you in, check server url, email and password!",
toastType: ToastType.error,
);
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/login/ui/change_password_form.dart';
class ChangePasswordPage extends HookConsumerWidget {
const ChangePasswordPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return const Scaffold(
body: ChangePasswordForm(),
);
}
}

View File

@@ -53,7 +53,8 @@ class CuratedLocation {
String toJson() => json.encode(toMap());
factory CuratedLocation.fromJson(String source) => CuratedLocation.fromMap(json.decode(source));
factory CuratedLocation.fromJson(String source) =>
CuratedLocation.fromMap(json.decode(source));
@override
String toString() {
@@ -74,6 +75,10 @@ class CuratedLocation {
@override
int get hashCode {
return id.hashCode ^ city.hashCode ^ resizePath.hashCode ^ deviceAssetId.hashCode ^ deviceId.hashCode;
return id.hashCode ^
city.hashCode ^
resizePath.hashCode ^
deviceAssetId.hashCode ^
deviceId.hashCode;
}
}

View File

@@ -54,7 +54,8 @@ class CuratedObject {
String toJson() => json.encode(toMap());
factory CuratedObject.fromJson(String source) => CuratedObject.fromMap(json.decode(source));
factory CuratedObject.fromJson(String source) =>
CuratedObject.fromMap(json.decode(source));
@override
String toString() {
@@ -75,6 +76,10 @@ class CuratedObject {
@override
int get hashCode {
return id.hashCode ^ object.hashCode ^ resizePath.hashCode ^ deviceAssetId.hashCode ^ deviceId.hashCode;
return id.hashCode ^
object.hashCode ^
resizePath.hashCode ^
deviceAssetId.hashCode ^
deviceId.hashCode;
}
}

View File

@@ -25,7 +25,8 @@ class SearchPageState {
searchTerm: searchTerm ?? this.searchTerm,
isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled,
searchSuggestion: searchSuggestion ?? this.searchSuggestion,
userSuggestedSearchTerms: userSuggestedSearchTerms ?? this.userSuggestedSearchTerms,
userSuggestedSearchTerms:
userSuggestedSearchTerms ?? this.userSuggestedSearchTerms,
);
}
@@ -43,13 +44,15 @@ class SearchPageState {
searchTerm: map['searchTerm'] ?? '',
isSearchEnabled: map['isSearchEnabled'] ?? false,
searchSuggestion: List<String>.from(map['searchSuggestion']),
userSuggestedSearchTerms: List<String>.from(map['userSuggestedSearchTerms']),
userSuggestedSearchTerms:
List<String>.from(map['userSuggestedSearchTerms']),
);
}
String toJson() => json.encode(toMap());
factory SearchPageState.fromJson(String source) => SearchPageState.fromMap(json.decode(source));
factory SearchPageState.fromJson(String source) =>
SearchPageState.fromMap(json.decode(source));
@override
String toString() {

View File

@@ -44,13 +44,15 @@ class SearchResultPageState {
isLoading: map['isLoading'] ?? false,
isSuccess: map['isSuccess'] ?? false,
isError: map['isError'] ?? false,
searchResult: List<ImmichAsset>.from(map['searchResult']?.map((x) => ImmichAsset.fromMap(x))),
searchResult: List<ImmichAsset>.from(
map['searchResult']?.map((x) => ImmichAsset.fromMap(x))),
);
}
String toJson() => json.encode(toMap());
factory SearchResultPageState.fromJson(String source) => SearchResultPageState.fromMap(json.decode(source));
factory SearchResultPageState.fromJson(String source) =>
SearchResultPageState.fromMap(json.decode(source));
@override
String toString() {
@@ -71,6 +73,9 @@ class SearchResultPageState {
@override
int get hashCode {
return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ searchResult.hashCode;
return isLoading.hashCode ^
isSuccess.hashCode ^
isError.hashCode ^
searchResult.hashCode;
}
}

View File

@@ -6,7 +6,7 @@ import 'package:immich_mobile/modules/search/models/search_page_state.model.dart
import 'package:immich_mobile/modules/search/services/search.service.dart';
class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
SearchPageStateNotifier()
SearchPageStateNotifier(this._searchService)
: super(
SearchPageState(
searchTerm: "",
@@ -16,7 +16,7 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
),
);
final SearchService _searchService = SearchService();
final SearchService _searchService;
void enableSearch() {
state = state.copyWith(isSearchEnabled: true);
@@ -54,29 +54,22 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
final searchPageStateProvider =
StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
return SearchPageStateNotifier();
return SearchPageStateNotifier(ref.watch(searchServiceProvider));
});
final getCuratedLocationProvider =
FutureProvider.autoDispose<List<CuratedLocation>>((ref) async {
final SearchService searchService = SearchService();
final SearchService searchService = ref.watch(searchServiceProvider);
var curatedLocation = await searchService.getCuratedLocation();
if (curatedLocation != null) {
return curatedLocation;
} else {
return [];
}
return curatedLocation ?? [];
});
final getCuratedObjectProvider =
FutureProvider.autoDispose<List<CuratedObject>>((ref) async {
final SearchService searchService = SearchService();
final SearchService searchService = ref.watch(searchServiceProvider);
var curatedObject = await searchService.getCuratedObjects();
if (curatedObject != null) {
return curatedObject;
} else {
return [];
}
return curatedObject ?? [];
});

View File

@@ -7,31 +7,48 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:intl/intl.dart';
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
SearchResultPageNotifier()
: super(SearchResultPageState(searchResult: [], isError: false, isLoading: true, isSuccess: false));
SearchResultPageNotifier(this._searchService)
: super(
SearchResultPageState(
searchResult: [],
isError: false,
isLoading: true,
isSuccess: false,
),
);
final SearchService _searchService = SearchService();
final SearchService _searchService;
void search(String searchTerm) async {
state = state.copyWith(searchResult: [], isError: false, isLoading: true, isSuccess: false);
state = state.copyWith(
searchResult: [], isError: false, isLoading: true, isSuccess: false);
List<ImmichAsset>? assets = await _searchService.searchAsset(searchTerm);
if (assets != null) {
state = state.copyWith(searchResult: assets, isError: false, isLoading: false, isSuccess: true);
state = state.copyWith(
searchResult: assets,
isError: false,
isLoading: false,
isSuccess: true);
} else {
state = state.copyWith(searchResult: [], isError: true, isLoading: false, isSuccess: false);
state = state.copyWith(
searchResult: [], isError: true, isLoading: false, isSuccess: false);
}
}
}
final searchResultPageProvider = StateNotifierProvider<SearchResultPageNotifier, SearchResultPageState>((ref) {
return SearchResultPageNotifier();
final searchResultPageProvider =
StateNotifierProvider<SearchResultPageNotifier, SearchResultPageState>(
(ref) {
return SearchResultPageNotifier(ref.watch(searchServiceProvider));
});
final searchResultGroupByDateTimeProvider = StateProvider((ref) {
var assets = ref.watch(searchResultPageProvider).searchResult;
assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
assets.sortByCompare<DateTime>(
(e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
return assets.groupListsBy((element) =>
DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
});

View File

@@ -1,13 +1,18 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/shared/services/network.service.dart';
final searchServiceProvider =
Provider((ref) => SearchService(ref.watch(networkServiceProvider)));
class SearchService {
final NetworkService _networkService = NetworkService();
final NetworkService _networkService;
SearchService(this._networkService);
Future<List<String>?> getUserSuggestedSearchTerms() async {
try {
@@ -30,7 +35,8 @@ class SearchService {
List<dynamic> decodedData = jsonDecode(res.toString());
List<ImmichAsset> result = List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
List<ImmichAsset> result =
List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
return result;
} catch (e) {
@@ -45,7 +51,8 @@ class SearchService {
List<dynamic> decodedData = jsonDecode(res.toString());
List<CuratedLocation> result = List.from(decodedData.map((a) => CuratedLocation.fromMap(a)));
List<CuratedLocation> result =
List.from(decodedData.map((a) => CuratedLocation.fromMap(a)));
return result;
} catch (e) {
@@ -60,7 +67,8 @@ class SearchService {
List<dynamic> decodedData = jsonDecode(res.toString());
List<CuratedObject> result = List.from(decodedData.map((a) => CuratedObject.fromMap(a)));
List<CuratedObject> result =
List.from(decodedData.map((a) => CuratedObject.fromMap(a)));
return result;
} catch (e) {

View File

@@ -4,7 +4,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
SearchBar({Key? key, required this.searchFocusNode, required this.onSubmitted}) : super(key: key);
SearchBar(
{Key? key, required this.searchFocusNode, required this.onSubmitted})
: super(key: key);
final FocusNode searchFocusNode;
final Function(String) onSubmitted;

View File

@@ -3,16 +3,20 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
class SearchSuggestionList extends ConsumerWidget {
const SearchSuggestionList({Key? key, required this.onSubmitted}) : super(key: key);
const SearchSuggestionList({Key? key, required this.onSubmitted})
: super(key: key);
final Function(String) onSubmitted;
@override
Widget build(BuildContext context, WidgetRef ref) {
final searchTerm = ref.watch(searchPageStateProvider).searchTerm;
final searchSuggestion = ref.watch(searchPageStateProvider).searchSuggestion;
final searchSuggestion =
ref.watch(searchPageStateProvider).searchSuggestion;
return Container(
color: searchTerm.isEmpty ? Colors.black.withOpacity(0.5) : Theme.of(context).scaffoldBackgroundColor,
color: searchTerm.isEmpty
? Colors.black.withOpacity(0.5)
: Theme.of(context).scaffoldBackgroundColor,
child: CustomScrollView(
slivers: [
SliverFillRemaining(

View File

@@ -5,7 +5,11 @@ import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
class ThumbnailWithInfo extends StatelessWidget {
const ThumbnailWithInfo({Key? key, required this.textInfo, required this.imageUrl, required this.onTap})
const ThumbnailWithInfo(
{Key? key,
required this.textInfo,
required this.imageUrl,
required this.onTap})
: super(key: key);
final String textInfo;
@@ -39,7 +43,9 @@ class ThumbnailWithInfo extends StatelessWidget {
height: 250,
fit: BoxFit.cover,
imageUrl: imageUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
httpHeaders: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
),
),
),

View File

@@ -18,7 +18,7 @@ import 'package:immich_mobile/utils/capitalize_first_letter.dart';
class SearchPage extends HookConsumerWidget {
SearchPage({Key? key}) : super(key: key);
late FocusNode searchFocusNode;
FocusNode searchFocusNode = FocusNode();
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -176,9 +176,8 @@ class SearchPage extends HookConsumerWidget {
_buildThings()
],
),
isSearchEnabled
? SearchSuggestionList(onSubmitted: _onSearchSubmitted)
: Container(),
if (isSearchEnabled)
SearchSuggestionList(onSubmitted: _onSearchSubmitted),
],
),
),

View File

@@ -166,7 +166,7 @@ class SearchResultPage extends HookConsumerWidget {
}
}
return Container();
return const SizedBox();
}
return Scaffold(
@@ -198,9 +198,8 @@ class SearchResultPage extends HookConsumerWidget {
child: Stack(
children: [
_buildSearchResult(),
isNewSearch.value
? SearchSuggestionList(onSubmitted: _onSearchSubmitted)
: Container(),
if (isNewSearch.value)
SearchSuggestionList(onSubmitted: _onSearchSubmitted),
],
),
),

View File

@@ -36,16 +36,20 @@ class AlbumViewerPageState {
String toJson() => json.encode(toMap());
factory AlbumViewerPageState.fromJson(String source) => AlbumViewerPageState.fromMap(json.decode(source));
factory AlbumViewerPageState.fromJson(String source) =>
AlbumViewerPageState.fromMap(json.decode(source));
@override
String toString() => 'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)';
String toString() =>
'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is AlbumViewerPageState && other.isEditAlbum == isEditAlbum && other.editTitleText == editTitleText;
return other is AlbumViewerPageState &&
other.isEditAlbum == isEditAlbum &&
other.editTitleText == editTitleText;
}
@override

View File

@@ -22,7 +22,8 @@ class AssetSelectionPageResult {
}) {
return AssetSelectionPageResult(
selectedNewAsset: selectedNewAsset ?? this.selectedNewAsset,
selectedAdditionalAsset: selectedAdditionalAsset ?? this.selectedAdditionalAsset,
selectedAdditionalAsset:
selectedAdditionalAsset ?? this.selectedAdditionalAsset,
isAlbumExist: isAlbumExist ?? this.isAlbumExist,
);
}
@@ -30,8 +31,12 @@ class AssetSelectionPageResult {
Map<String, dynamic> toMap() {
final result = <String, dynamic>{};
result.addAll({'selectedNewAsset': selectedNewAsset.map((x) => x.toMap()).toList()});
result.addAll({'selectedAdditionalAsset': selectedAdditionalAsset.map((x) => x.toMap()).toList()});
result.addAll(
{'selectedNewAsset': selectedNewAsset.map((x) => x.toMap()).toList()});
result.addAll({
'selectedAdditionalAsset':
selectedAdditionalAsset.map((x) => x.toMap()).toList()
});
result.addAll({'isAlbumExist': isAlbumExist});
return result;
@@ -39,16 +44,18 @@ class AssetSelectionPageResult {
factory AssetSelectionPageResult.fromMap(Map<String, dynamic> map) {
return AssetSelectionPageResult(
selectedNewAsset: Set<ImmichAsset>.from(map['selectedNewAsset']?.map((x) => ImmichAsset.fromMap(x))),
selectedAdditionalAsset:
Set<ImmichAsset>.from(map['selectedAdditionalAsset']?.map((x) => ImmichAsset.fromMap(x))),
selectedNewAsset: Set<ImmichAsset>.from(
map['selectedNewAsset']?.map((x) => ImmichAsset.fromMap(x))),
selectedAdditionalAsset: Set<ImmichAsset>.from(
map['selectedAdditionalAsset']?.map((x) => ImmichAsset.fromMap(x))),
isAlbumExist: map['isAlbumExist'] ?? false,
);
}
String toJson() => json.encode(toMap());
factory AssetSelectionPageResult.fromJson(String source) => AssetSelectionPageResult.fromMap(json.decode(source));
factory AssetSelectionPageResult.fromJson(String source) =>
AssetSelectionPageResult.fromMap(json.decode(source));
@override
String toString() =>
@@ -66,5 +73,8 @@ class AssetSelectionPageResult {
}
@override
int get hashCode => selectedNewAsset.hashCode ^ selectedAdditionalAsset.hashCode ^ isAlbumExist.hashCode;
int get hashCode =>
selectedNewAsset.hashCode ^
selectedAdditionalAsset.hashCode ^
isAlbumExist.hashCode;
}

View File

@@ -32,9 +32,12 @@ class AssetSelectionState {
}) {
return AssetSelectionState(
selectedMonths: selectedMonths ?? this.selectedMonths,
selectedNewAssetsForAlbum: selectedNewAssetsForAlbum ?? this.selectedNewAssetsForAlbum,
selectedAdditionalAssetsForAlbum: selectedAdditionalAssetsForAlbum ?? this.selectedAdditionalAssetsForAlbum,
selectedAssetsInAlbumViewer: selectedAssetsInAlbumViewer ?? this.selectedAssetsInAlbumViewer,
selectedNewAssetsForAlbum:
selectedNewAssetsForAlbum ?? this.selectedNewAssetsForAlbum,
selectedAdditionalAssetsForAlbum: selectedAdditionalAssetsForAlbum ??
this.selectedAdditionalAssetsForAlbum,
selectedAssetsInAlbumViewer:
selectedAssetsInAlbumViewer ?? this.selectedAssetsInAlbumViewer,
isMultiselectEnable: isMultiselectEnable ?? this.isMultiselectEnable,
isAlbumExist: isAlbumExist ?? this.isAlbumExist,
);
@@ -44,10 +47,18 @@ class AssetSelectionState {
final result = <String, dynamic>{};
result.addAll({'selectedMonths': selectedMonths.toList()});
result.addAll({'selectedNewAssetsForAlbum': selectedNewAssetsForAlbum.map((x) => x.toMap()).toList()});
result
.addAll({'selectedAdditionalAssetsForAlbum': selectedAdditionalAssetsForAlbum.map((x) => x.toMap()).toList()});
result.addAll({'selectedAssetsInAlbumViewer': selectedAssetsInAlbumViewer.map((x) => x.toMap()).toList()});
result.addAll({
'selectedNewAssetsForAlbum':
selectedNewAssetsForAlbum.map((x) => x.toMap()).toList()
});
result.addAll({
'selectedAdditionalAssetsForAlbum':
selectedAdditionalAssetsForAlbum.map((x) => x.toMap()).toList()
});
result.addAll({
'selectedAssetsInAlbumViewer':
selectedAssetsInAlbumViewer.map((x) => x.toMap()).toList()
});
result.addAll({'isMultiselectEnable': isMultiselectEnable});
result.addAll({'isAlbumExist': isAlbumExist});
@@ -57,12 +68,14 @@ class AssetSelectionState {
factory AssetSelectionState.fromMap(Map<String, dynamic> map) {
return AssetSelectionState(
selectedMonths: Set<String>.from(map['selectedMonths']),
selectedNewAssetsForAlbum:
Set<ImmichAsset>.from(map['selectedNewAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))),
selectedAdditionalAssetsForAlbum:
Set<ImmichAsset>.from(map['selectedAdditionalAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))),
selectedAssetsInAlbumViewer:
Set<ImmichAsset>.from(map['selectedAssetsInAlbumViewer']?.map((x) => ImmichAsset.fromMap(x))),
selectedNewAssetsForAlbum: Set<ImmichAsset>.from(
map['selectedNewAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))),
selectedAdditionalAssetsForAlbum: Set<ImmichAsset>.from(
map['selectedAdditionalAssetsForAlbum']
?.map((x) => ImmichAsset.fromMap(x))),
selectedAssetsInAlbumViewer: Set<ImmichAsset>.from(
map['selectedAssetsInAlbumViewer']
?.map((x) => ImmichAsset.fromMap(x))),
isMultiselectEnable: map['isMultiselectEnable'] ?? false,
isAlbumExist: map['isAlbumExist'] ?? false,
);
@@ -70,7 +83,8 @@ class AssetSelectionState {
String toJson() => json.encode(toMap());
factory AssetSelectionState.fromJson(String source) => AssetSelectionState.fromMap(json.decode(source));
factory AssetSelectionState.fromJson(String source) =>
AssetSelectionState.fromMap(json.decode(source));
@override
String toString() {
@@ -85,8 +99,10 @@ class AssetSelectionState {
return other is AssetSelectionState &&
setEquals(other.selectedMonths, selectedMonths) &&
setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) &&
setEquals(other.selectedAdditionalAssetsForAlbum, selectedAdditionalAssetsForAlbum) &&
setEquals(other.selectedAssetsInAlbumViewer, selectedAssetsInAlbumViewer) &&
setEquals(other.selectedAdditionalAssetsForAlbum,
selectedAdditionalAssetsForAlbum) &&
setEquals(
other.selectedAssetsInAlbumViewer, selectedAssetsInAlbumViewer) &&
other.isMultiselectEnable == isMultiselectEnable &&
other.isAlbumExist == isAlbumExist;
}

View File

@@ -38,7 +38,8 @@ class SharedAlbum {
ownerId: ownerId ?? this.ownerId,
albumName: albumName ?? this.albumName,
createdAt: createdAt ?? this.createdAt,
albumThumbnailAssetId: albumThumbnailAssetId ?? this.albumThumbnailAssetId,
albumThumbnailAssetId:
albumThumbnailAssetId ?? this.albumThumbnailAssetId,
sharedUsers: sharedUsers ?? this.sharedUsers,
assets: assets ?? this.assets,
);
@@ -69,16 +70,16 @@ class SharedAlbum {
albumName: map['albumName'] ?? '',
createdAt: map['createdAt'] ?? '',
albumThumbnailAssetId: map['albumThumbnailAssetId'],
sharedUsers: List<User>.from(map['sharedUsers']?.map((x) => User.fromMap(x))),
assets: map['assets'] != null
? List<ImmichAsset>.from(map['assets']?.map((x) => ImmichAsset.fromMap(x)))
: null,
sharedUsers:
List<User>.from(map['sharedUsers']?.map((x) => User.fromMap(x))),
assets: map['assets']?.map((x) => ImmichAsset.fromMap(x)).toList(),
);
}
String toJson() => json.encode(toMap());
factory SharedAlbum.fromJson(String source) => SharedAlbum.fromMap(json.decode(source));
factory SharedAlbum.fromJson(String source) =>
SharedAlbum.fromMap(json.decode(source));
@override
String toString() {

View File

@@ -12,4 +12,5 @@ class AlbumTitleNotifier extends StateNotifier<String> {
}
}
final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>((ref) => AlbumTitleNotifier());
final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>(
(ref) => AlbumTitleNotifier());

View File

@@ -4,7 +4,8 @@ import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.da
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
AlbumViewerNotifier(this.ref) : super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
AlbumViewerNotifier(this.ref)
: super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
final Ref ref;
@@ -28,10 +29,12 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
state = state.copyWith(editTitleText: "", isEditAlbum: false);
}
Future<bool> changeAlbumTitle(String albumId, String ownerId, String newAlbumTitle) async {
SharedAlbumService service = SharedAlbumService();
Future<bool> changeAlbumTitle(
String albumId, String ownerId, String newAlbumTitle) async {
SharedAlbumService service = ref.watch(sharedAlbumServiceProvider);
bool isSuccess = await service.changeTitleAlbum(albumId, ownerId, newAlbumTitle);
bool isSuccess =
await service.changeTitleAlbum(albumId, ownerId, newAlbumTitle);
if (isSuccess) {
state = state.copyWith(editTitleText: "", isEditAlbum: false);
@@ -45,6 +48,7 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
}
}
final albumViewerProvider = StateNotifierProvider<AlbumViewerNotifier, AlbumViewerPageState>((ref) {
final albumViewerProvider =
StateNotifierProvider<AlbumViewerNotifier, AlbumViewerPageState>((ref) {
return AlbumViewerNotifier(ref);
});

View File

@@ -18,35 +18,48 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
state = state.copyWith(isAlbumExist: isAlbumExist);
}
void removeAssetsInMonth(String removedMonth, List<ImmichAsset> assetsInMonth) {
void removeAssetsInMonth(
String removedMonth, List<ImmichAsset> assetsInMonth) {
Set<ImmichAsset> currentAssetList = state.selectedNewAssetsForAlbum;
Set<String> currentMonthList = state.selectedMonths;
currentMonthList.removeWhere((selectedMonth) => selectedMonth == removedMonth);
currentMonthList
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
for (ImmichAsset asset in assetsInMonth) {
currentAssetList.removeWhere((e) => e.id == asset.id);
}
state = state.copyWith(selectedNewAssetsForAlbum: currentAssetList, selectedMonths: currentMonthList);
state = state.copyWith(
selectedNewAssetsForAlbum: currentAssetList,
selectedMonths: currentMonthList);
}
void addAdditionalAssets(List<ImmichAsset> assets) {
state = state.copyWith(
selectedAdditionalAssetsForAlbum: {...state.selectedAdditionalAssetsForAlbum, ...assets},
selectedAdditionalAssetsForAlbum: {
...state.selectedAdditionalAssetsForAlbum,
...assets
},
);
}
void addAllAssetsInMonth(String month, List<ImmichAsset> assetsInMonth) {
state = state.copyWith(
selectedMonths: {...state.selectedMonths, month},
selectedNewAssetsForAlbum: {...state.selectedNewAssetsForAlbum, ...assetsInMonth},
selectedNewAssetsForAlbum: {
...state.selectedNewAssetsForAlbum,
...assetsInMonth
},
);
}
void addNewAssets(List<ImmichAsset> assets) {
state = state.copyWith(
selectedNewAssetsForAlbum: {...state.selectedNewAssetsForAlbum, ...assets},
selectedNewAssetsForAlbum: {
...state.selectedNewAssetsForAlbum,
...assets
},
);
}
@@ -93,7 +106,10 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
void addAssetsInAlbumViewer(List<ImmichAsset> assets) {
state = state.copyWith(
selectedAssetsInAlbumViewer: {...state.selectedAssetsInAlbumViewer, ...assets},
selectedAssetsInAlbumViewer: {
...state.selectedAssetsInAlbumViewer,
...assets
},
);
}
@@ -108,6 +124,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
}
}
final assetSelectionProvider = StateNotifierProvider<AssetSelectionNotifier, AssetSelectionState>((ref) {
final assetSelectionProvider =
StateNotifierProvider<AssetSelectionNotifier, AssetSelectionState>((ref) {
return AssetSelectionNotifier();
});

View File

@@ -3,9 +3,9 @@ import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
SharedAlbumNotifier() : super([]);
SharedAlbumNotifier(this._sharedAlbumService) : super([]);
final SharedAlbumService _sharedAlbumService = SharedAlbumService();
final SharedAlbumService _sharedAlbumService;
getAllSharedAlbums() async {
List<SharedAlbum> sharedAlbums =
@@ -50,12 +50,13 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
final sharedAlbumProvider =
StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) {
return SharedAlbumNotifier();
return SharedAlbumNotifier(ref.watch(sharedAlbumServiceProvider));
});
final sharedAlbumDetailProvider = FutureProvider.autoDispose
.family<SharedAlbum, String>((ref, albumId) async {
final SharedAlbumService sharedAlbumService = SharedAlbumService();
final SharedAlbumService sharedAlbumService =
ref.watch(sharedAlbumServiceProvider);
return await sharedAlbumService.getAlbumDetail(albumId);
});

View File

@@ -2,8 +2,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/user.model.dart';
import 'package:immich_mobile/shared/services/user.service.dart';
final suggestedSharedUsersProvider = FutureProvider.autoDispose<List<User>>((ref) async {
UserService userService = UserService();
final suggestedSharedUsersProvider =
FutureProvider.autoDispose<List<User>>((ref) async {
UserService userService = ref.watch(userServiceProvider);
return await userService.getAllUsersInfo();
});

View File

@@ -3,12 +3,17 @@ import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/shared/services/network.service.dart';
final sharedAlbumServiceProvider =
Provider((ref) => SharedAlbumService(ref.watch(networkServiceProvider)));
class SharedAlbumService {
final NetworkService _networkService = NetworkService();
final NetworkService _networkService;
SharedAlbumService(this._networkService);
Future<List<SharedAlbum>> getAllSharedAlbum() async {
try {
@@ -34,11 +39,7 @@ class SharedAlbumService {
"assetIds": assets.map((asset) => asset.id).toList(),
});
if (res == null) {
return false;
}
return true;
return res != null;
} catch (e) {
debugPrint("Error createSharedAlbum ${e.toString()}");
return false;
@@ -66,11 +67,7 @@ class SharedAlbumService {
"assetIds": assets.map((asset) => asset.id).toList(),
});
if (res == null) {
return false;
}
return true;
return res != null;
} catch (e) {
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
return false;
@@ -85,11 +82,7 @@ class SharedAlbumService {
"sharedUserIds": sharedUserIds,
});
if (res == null) {
return false;
}
return true;
return res != null;
} catch (e) {
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
return false;

View File

@@ -5,7 +5,11 @@ class AlbumActionOutlinedButton extends StatelessWidget {
final String labelText;
final IconData iconData;
const AlbumActionOutlinedButton({Key? key, this.onPressed, required this.labelText, required this.iconData})
const AlbumActionOutlinedButton(
{Key? key,
this.onPressed,
required this.labelText,
required this.iconData})
: super(key: key);
@override
@@ -26,7 +30,8 @@ class AlbumActionOutlinedButton extends StatelessWidget {
icon: Icon(iconData, size: 15),
label: Text(
labelText,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87),
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87),
),
onPressed: onPressed,
),

View File

@@ -29,7 +29,8 @@ class AlbumTitleTextField extends ConsumerWidget {
ref.watch(albumTitleProvider.notifier).setAlbumTitle(v);
},
focusNode: albumTitleTextFieldFocusNode,
style: TextStyle(fontSize: 28, color: Colors.grey[700], fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 28, color: Colors.grey[700], fontWeight: FontWeight.bold),
controller: albumTitleController,
onTap: () {
isAlbumTitleTextFieldFocus.value = true;

View File

@@ -114,7 +114,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
onTap: () => _onRemoveFromAlbumPressed(albumId),
);
} else {
return Container();
return const SizedBox();
}
} else {
if (_albumInfo.asData?.value.ownerId == userId) {
@@ -198,8 +198,8 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
elevation: 0,
leading: _buildLeadingButton(),
title: isMultiSelectionEnable
? Text(selectedAssetsInAlbum.length.toString())
: Container(),
? Text('${selectedAssetsInAlbum.length}')
: null,
centerTitle: false,
actions: [
IconButton(

View File

@@ -7,11 +7,14 @@ import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.da
class AlbumViewerEditableTitle extends HookConsumerWidget {
final SharedAlbum albumInfo;
final FocusNode titleFocusNode;
const AlbumViewerEditableTitle({Key? key, required this.albumInfo, required this.titleFocusNode}) : super(key: key);
const AlbumViewerEditableTitle(
{Key? key, required this.albumInfo, required this.titleFocusNode})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final titleTextEditController = useTextEditingController(text: albumInfo.albumName);
final titleTextEditController =
useTextEditingController(text: albumInfo.albumName);
void onFocusModeChange() {
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
@@ -40,7 +43,9 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
onTap: () {
FocusScope.of(context).requestFocus(titleFocusNode);
ref.watch(albumViewerProvider.notifier).setEditTitleText(albumInfo.albumName);
ref
.watch(albumViewerProvider.notifier)
.setEditTitleText(albumInfo.albumName);
ref.watch(albumViewerProvider.notifier).enableEditAlbum();
if (titleTextEditController.text == 'Untitled') {

View File

@@ -22,8 +22,10 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
var deviceId = ref.watch(authenticationProvider).deviceId;
final selectedAssetsInAlbumViewer = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable;
final selectedAssetsInAlbumViewer =
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
final isMultiSelectionEnable =
ref.watch(assetSelectionProvider).isMultiselectEnable;
_viewAsset() {
if (asset.type == 'IMAGE') {
@@ -39,7 +41,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
} else {
AutoRouter.of(context).push(
VideoViewerRoute(
videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
videoUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
asset: asset),
);
}
@@ -58,7 +61,9 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
_enableMultiSelection() {
ref.watch(assetSelectionProvider.notifier).enableMultiselection();
ref.watch(assetSelectionProvider.notifier).addAssetsInAlbumViewer([asset]);
ref
.watch(assetSelectionProvider.notifier)
.addAssetsInAlbumViewer([asset]);
}
_disableMultiSelection() {
@@ -66,9 +71,6 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
}
_buildVideoLabel() {
if (asset.type == 'IMAGE') {
return Container();
} else {
return Positioned(
top: 5,
right: 5,
@@ -89,14 +91,15 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
),
);
}
}
_buildAssetStoreLocationIcon() {
return Positioned(
right: 10,
bottom: 5,
child: Icon(
(deviceId != asset.deviceId) ? Icons.cloud_done_outlined : Icons.photo_library_rounded,
(deviceId != asset.deviceId)
? Icons.cloud_done_outlined
: Icons.photo_library_rounded,
color: Colors.white,
size: 18,
),
@@ -105,7 +108,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
_buildAssetSelectionIcon() {
bool isSelected = selectedAssetsInAlbumViewer.contains(asset);
if (isMultiSelectionEnable) {
return Positioned(
left: 10,
top: 5,
@@ -119,9 +122,6 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
color: Colors.white,
),
);
} else {
return Container();
}
}
_buildThumbnailImage() {
@@ -136,7 +136,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
imageUrl: thumbnailRequestUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
progressIndicatorBuilder: (context, url, downloadProgress) =>
Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress),
),
@@ -152,13 +153,17 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
_handleSelectionGesture() {
if (selectedAssetsInAlbumViewer.contains(asset)) {
ref.watch(assetSelectionProvider.notifier).removeAssetsInAlbumViewer([asset]);
ref
.watch(assetSelectionProvider.notifier)
.removeAssetsInAlbumViewer([asset]);
if (selectedAssetsInAlbumViewer.isEmpty) {
_disableMultiSelection();
}
} else {
ref.watch(assetSelectionProvider.notifier).addAssetsInAlbumViewer([asset]);
ref
.watch(assetSelectionProvider.notifier)
.addAssetsInAlbumViewer([asset]);
}
}
@@ -171,8 +176,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
children: [
_buildThumbnailImage(),
_buildAssetStoreLocationIcon(),
_buildVideoLabel(),
_buildAssetSelectionIcon(),
if (asset.type != 'IMAGE') _buildVideoLabel(),
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
],
),
),

View File

@@ -5,7 +5,8 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class AssetGridByMonth extends HookConsumerWidget {
final List<ImmichAsset> assetGroup;
const AssetGridByMonth({Key? key, required this.assetGroup}) : super(key: key);
const AssetGridByMonth({Key? key, required this.assetGroup})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return SliverGrid(

View File

@@ -8,12 +8,15 @@ class MonthGroupTitle extends HookConsumerWidget {
final String month;
final List<ImmichAsset> assetGroup;
const MonthGroupTitle({Key? key, required this.month, required this.assetGroup}) : super(key: key);
const MonthGroupTitle(
{Key? key, required this.month, required this.assetGroup})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedDateGroup = ref.watch(assetSelectionProvider).selectedMonths;
final selectedAssets = ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
final selectedAssets =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
_handleTitleIconClick() {
@@ -21,10 +24,16 @@ class MonthGroupTitle extends HookConsumerWidget {
if (isAlbumExist) {
if (selectedDateGroup.contains(month)) {
ref.watch(assetSelectionProvider.notifier).removeAssetsInMonth(month, []);
ref.watch(assetSelectionProvider.notifier).removeSelectedAdditionalAssets(assetGroup);
ref
.watch(assetSelectionProvider.notifier)
.removeAssetsInMonth(month, []);
ref
.watch(assetSelectionProvider.notifier)
.removeSelectedAdditionalAssets(assetGroup);
} else {
ref.watch(assetSelectionProvider.notifier).addAllAssetsInMonth(month, []);
ref
.watch(assetSelectionProvider.notifier)
.addAllAssetsInMonth(month, []);
// Deep clone assetGroup
var assetGroupWithNewItems = [...assetGroup];
@@ -33,13 +42,19 @@ class MonthGroupTitle extends HookConsumerWidget {
assetGroupWithNewItems.removeWhere((a) => a.id == selectedAsset.id);
}
ref.watch(assetSelectionProvider.notifier).addAdditionalAssets(assetGroupWithNewItems);
ref
.watch(assetSelectionProvider.notifier)
.addAdditionalAssets(assetGroupWithNewItems);
}
} else {
if (selectedDateGroup.contains(month)) {
ref.watch(assetSelectionProvider.notifier).removeAssetsInMonth(month, assetGroup);
ref
.watch(assetSelectionProvider.notifier)
.removeAssetsInMonth(month, assetGroup);
} else {
ref.watch(assetSelectionProvider.notifier).addAllAssetsInMonth(month, assetGroup);
ref
.watch(assetSelectionProvider.notifier)
.addAllAssetsInMonth(month, assetGroup);
}
}
}
@@ -59,7 +74,8 @@ class MonthGroupTitle extends HookConsumerWidget {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 29.0, bottom: 29.0, left: 14.0, right: 8.0),
padding: const EdgeInsets.only(
top: 29.0, bottom: 29.0, left: 14.0, right: 8.0),
child: Row(
children: [
GestureDetector(

View File

@@ -10,7 +10,8 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class SelectionThumbnailImage extends HookConsumerWidget {
final ImmichAsset asset;
const SelectionThumbnailImage({Key? key, required this.asset}) : super(key: key);
const SelectionThumbnailImage({Key? key, required this.asset})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -18,8 +19,10 @@ class SelectionThumbnailImage extends HookConsumerWidget {
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
var selectedAsset = ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
var newAssetsForAlbum = ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
var selectedAsset =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
var newAssetsForAlbum =
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
Widget _buildSelectionIcon(ImmichAsset asset) {
@@ -72,15 +75,21 @@ class SelectionThumbnailImage extends HookConsumerWidget {
// Operation for existing album
if (!selectedAsset.contains(asset)) {
if (newAssetsForAlbum.contains(asset)) {
ref.watch(assetSelectionProvider.notifier).removeSelectedAdditionalAssets([asset]);
ref
.watch(assetSelectionProvider.notifier)
.removeSelectedAdditionalAssets([asset]);
} else {
ref.watch(assetSelectionProvider.notifier).addAdditionalAssets([asset]);
ref
.watch(assetSelectionProvider.notifier)
.addAdditionalAssets([asset]);
}
}
} else {
// Operation for new album
if (selectedAsset.contains(asset)) {
ref.watch(assetSelectionProvider.notifier).removeSelectedNewAssets([asset]);
ref
.watch(assetSelectionProvider.notifier)
.removeSelectedNewAssets([asset]);
} else {
ref.watch(assetSelectionProvider.notifier).addNewAssets([asset]);
}
@@ -97,11 +106,15 @@ class SelectionThumbnailImage extends HookConsumerWidget {
memCacheHeight: asset.type == 'IMAGE' ? 150 : 150,
fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
httpHeaders: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
progressIndicatorBuilder: (context, url, downloadProgress) =>
Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress),
child:
CircularProgressIndicator(value: downloadProgress.progress),
),
errorWidget: (context, url, error) {
return Icon(
@@ -118,15 +131,14 @@ class SelectionThumbnailImage extends HookConsumerWidget {
child: _buildSelectionIcon(asset),
),
),
asset.type == 'IMAGE'
? Container()
: Positioned(
if (asset.type != 'IMAGE')
Positioned(
bottom: 5,
right: 5,
child: Row(
children: [
Text(
asset.duration.toString().substring(0, 7),
'${asset.duration?.substring(0, 7)}',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
@@ -138,7 +150,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
),
],
),
)
),
],
),
);

View File

@@ -9,7 +9,8 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class SharedAlbumThumbnailImage extends HookConsumerWidget {
final ImmichAsset asset;
const SharedAlbumThumbnailImage({Key? key, required this.asset}) : super(key: key);
const SharedAlbumThumbnailImage({Key? key, required this.asset})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -34,9 +35,11 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
imageUrl: thumbnailRequestUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
progressIndicatorBuilder: (context, url, downloadProgress) =>
Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress),
child:
CircularProgressIndicator(value: downloadProgress.progress),
),
errorWidget: (context, url, error) {
return Icon(

View File

@@ -14,7 +14,8 @@ class SharingSliverAppBar extends StatelessWidget {
floating: false,
pinned: true,
snap: false,
leading: Container(),
automaticallyImplyLeading: false,
// leading: Container(),
// elevation: 0,
title: Text(
'IMMICH',
@@ -37,11 +38,13 @@ class SharingSliverAppBar extends StatelessWidget {
padding: const EdgeInsets.only(right: 4.0),
child: TextButton.icon(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Theme.of(context).primaryColor.withAlpha(20)),
backgroundColor: MaterialStateProperty.all(
Theme.of(context).primaryColor.withAlpha(20)),
// foregroundColor: MaterialStateProperty.all(Colors.white),
),
onPressed: () {
AutoRouter.of(context).push(const CreateSharedAlbumRoute());
AutoRouter.of(context)
.push(const CreateSharedAlbumRoute());
},
icon: const Icon(
Icons.photo_album_outlined,
@@ -49,7 +52,8 @@ class SharingSliverAppBar extends StatelessWidget {
),
label: const Text(
"Create shared album",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
),
),
),
@@ -59,7 +63,8 @@ class SharingSliverAppBar extends StatelessWidget {
padding: const EdgeInsets.only(left: 4.0),
child: TextButton.icon(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Theme.of(context).primaryColor.withAlpha(20)),
backgroundColor: MaterialStateProperty.all(
Theme.of(context).primaryColor.withAlpha(20)),
// foregroundColor: MaterialStateProperty.all(Colors.white),
),
onPressed: null,
@@ -69,7 +74,8 @@ class SharingSliverAppBar extends StatelessWidget {
),
label: const Text(
"Share with partner",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
),
),
),

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(SharedAlbum albumInfo) async {
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) {
if (albumInfo.assets?.isNotEmpty == true) {
ref
.watch(assetSelectionProvider.notifier)
.addNewAssets(albumInfo.assets!.toList());
@@ -53,7 +53,9 @@ class AlbumViewerPage extends HookConsumerWidget {
if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
ImmichLoadingOverlayController.appLoader.show();
var isSuccess = await SharedAlbumService().addAdditionalAssetToAlbum(
var isSuccess = await ref
.watch(sharedAlbumServiceProvider)
.addAdditionalAssetToAlbum(
returnPayload.selectedAdditionalAsset, albumId);
if (isSuccess) {
@@ -77,7 +79,8 @@ class AlbumViewerPage extends HookConsumerWidget {
if (sharedUserIds != null) {
ImmichLoadingOverlayController.appLoader.show();
var isSuccess = await SharedAlbumService()
var isSuccess = await ref
.watch(sharedAlbumServiceProvider)
.addAdditionalUserToAlbum(sharedUserIds, albumId);
if (isSuccess) {
@@ -106,12 +109,11 @@ class AlbumViewerPage extends HookConsumerWidget {
}
Widget _buildAlbumDateRange(SharedAlbum albumInfo) {
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) {
String startDate = "";
DateTime parsedStartDate =
DateTime.parse(albumInfo.assets!.first.createdAt);
DateTime parsedEndDate =
DateTime.parse(albumInfo.assets!.last.createdAt);
DateTime parsedEndDate = DateTime.parse(
albumInfo.assets?.last.createdAt ?? '11111111'); //Need default.
if (parsedStartDate.year == parsedEndDate.year) {
startDate = DateFormat('LLL d').format(parsedStartDate);
@@ -129,9 +131,6 @@ class AlbumViewerPage extends HookConsumerWidget {
fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey),
),
);
} else {
return Container();
}
}
Widget _buildHeader(SharedAlbum albumInfo) {
@@ -140,6 +139,7 @@ class AlbumViewerPage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(albumInfo),
if (albumInfo.assets?.isNotEmpty == true)
_buildAlbumDateRange(albumInfo),
SizedBox(
height: 60,
@@ -172,7 +172,7 @@ class AlbumViewerPage extends HookConsumerWidget {
}
Widget _buildImageGrid(SharedAlbum albumInfo) {
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) {
if (albumInfo.assets?.isNotEmpty == true) {
return SliverPadding(
padding: const EdgeInsets.only(top: 10.0),
sliver: SliverGrid(
@@ -206,13 +206,12 @@ class AlbumViewerPage extends HookConsumerWidget {
onPressed: () => _onAddPhotosPressed(albumInfo),
labelText: "Add photos",
),
userId == albumInfo.ownerId
? AlbumActionOutlinedButton(
if (userId == albumInfo.ownerId)
AlbumActionOutlinedButton(
iconData: Icons.person_add_alt_rounded,
onPressed: () => _onAddUsersPressed(albumInfo),
labelText: "Add users",
)
: Container(),
),
],
),
),

View File

@@ -74,9 +74,9 @@ class AssetSelectionPage extends HookConsumerWidget {
),
centerTitle: false,
actions: [
(!isAlbumExist && selectedAssets.isNotEmpty) ||
(isAlbumExist && newAssetsForAlbum.isNotEmpty)
? TextButton(
if ((!isAlbumExist && selectedAssets.isNotEmpty) ||
(isAlbumExist && newAssetsForAlbum.isNotEmpty))
TextButton(
onPressed: () {
var payload = AssetSelectionPageResult(
isAlbumExist: isAlbumExist,
@@ -89,8 +89,7 @@ class AssetSelectionPage extends HookConsumerWidget {
"Add",
style: TextStyle(fontWeight: FontWeight.bold),
),
)
: Container()
),
],
),
body: _buildBody(),

View File

@@ -113,7 +113,6 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
}
_buildControlButton() {
if (selectedAssets.isNotEmpty) {
return Padding(
padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16),
child: SizedBox(
@@ -132,9 +131,6 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
);
}
return Container();
}
_buildSelectedImageGrid() {
if (selectedAssets.isNotEmpty) {
return SliverPadding(
@@ -196,7 +192,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
slivers: [
SliverAppBar(
elevation: 5,
leading: Container(),
automaticallyImplyLeading: false,
// leading: Container(),
pinned: true,
floating: false,
bottom: PreferredSize(
@@ -204,7 +201,7 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
child: Column(
children: [
_buildTitleInputField(),
_buildControlButton(),
if (selectedAssets.isNotEmpty) _buildControlButton(),
],
),
),

View File

@@ -10,15 +10,18 @@ import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
final SharedAlbum albumInfo;
const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo}) : super(key: key);
const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<List<User>> suggestedShareUsers = ref.watch(suggestedSharedUsersProvider);
AsyncValue<List<User>> suggestedShareUsers =
ref.watch(suggestedSharedUsersProvider);
final sharedUsersList = useState<Set<User>>({});
_addNewUsersHandler() {
AutoRouter.of(context).pop(sharedUsersList.value.map((e) => e.id).toList());
AutoRouter.of(context)
.pop(sharedUsersList.value.map((e) => e.id).toList());
}
_buildTileIcon(User user) {
@@ -32,7 +35,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
);
} else {
return CircleAvatar(
backgroundImage: const AssetImage('assets/immich-logo-no-outline.png'),
backgroundImage:
const AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Theme.of(context).primaryColor.withAlpha(50),
);
}
@@ -49,7 +53,10 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
label: Text(
user.email,
style: const TextStyle(fontSize: 12, color: Colors.black87, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 12,
color: Colors.black87,
fontWeight: FontWeight.bold),
),
),
),
@@ -65,7 +72,10 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
padding: EdgeInsets.all(16.0),
child: Text(
'Suggestions',
style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 14,
color: Colors.grey,
fontWeight: FontWeight.bold),
),
),
ListView.builder(
@@ -75,14 +85,20 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
leading: _buildTileIcon(users[index]),
title: Text(
users[index].email,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold),
),
onTap: () {
if (sharedUsersList.value.contains(users[index])) {
sharedUsersList.value =
sharedUsersList.value.where((selectedUser) => selectedUser.id != users[index].id).toSet();
sharedUsersList.value = sharedUsersList.value
.where((selectedUser) =>
selectedUser.id != users[index].id)
.toSet();
} else {
sharedUsersList.value = {...sharedUsersList.value, users[index]};
sharedUsersList.value = {
...sharedUsersList.value,
users[index]
};
}
},
);
@@ -109,7 +125,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
),
actions: [
TextButton(
onPressed: sharedUsersList.value.isEmpty ? null : _addNewUsersHandler,
onPressed:
sharedUsersList.value.isEmpty ? null : _addNewUsersHandler,
child: const Text(
"Add",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
@@ -120,7 +137,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
body: suggestedShareUsers.when(
data: (users) {
for (var sharedUsers in albumInfo.sharedUsers) {
users.removeWhere((u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId);
users.removeWhere(
(u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId);
}
return _buildUserList(users);

View File

@@ -16,10 +16,12 @@ class SelectUserForSharingPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final sharedUsersList = useState<Set<User>>({});
AsyncValue<List<User>> suggestedShareUsers = ref.watch(suggestedSharedUsersProvider);
AsyncValue<List<User>> suggestedShareUsers =
ref.watch(suggestedSharedUsersProvider);
_createSharedAlbum() async {
var isSuccess = await SharedAlbumService().createSharedAlbum(
var isSuccess =
await ref.watch(sharedAlbumServiceProvider).createSharedAlbum(
ref.watch(albumTitleProvider),
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
sharedUsersList.value.map((userInfo) => userInfo.id).toList(),
@@ -30,10 +32,12 @@ class SelectUserForSharingPage extends HookConsumerWidget {
ref.watch(assetSelectionProvider.notifier).removeAll();
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
AutoRouter.of(context).navigate(const TabControllerRoute(children: [SharingRoute()]));
AutoRouter.of(context)
.navigate(const TabControllerRoute(children: [SharingRoute()]));
}
const ScaffoldMessenger(child: SnackBar(content: Text('Failed to create album')));
const ScaffoldMessenger(
child: SnackBar(content: Text('Failed to create album')));
}
_buildTileIcon(User user) {
@@ -47,7 +51,8 @@ class SelectUserForSharingPage extends HookConsumerWidget {
);
} else {
return CircleAvatar(
backgroundImage: const AssetImage('assets/immich-logo-no-outline.png'),
backgroundImage:
const AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Theme.of(context).primaryColor.withAlpha(50),
);
}
@@ -64,7 +69,10 @@ class SelectUserForSharingPage extends HookConsumerWidget {
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
label: Text(
user.email,
style: const TextStyle(fontSize: 12, color: Colors.black87, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 12,
color: Colors.black87,
fontWeight: FontWeight.bold),
),
),
),
@@ -80,7 +88,10 @@ class SelectUserForSharingPage extends HookConsumerWidget {
padding: EdgeInsets.all(16.0),
child: Text(
'Suggestions',
style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 14,
color: Colors.grey,
fontWeight: FontWeight.bold),
),
),
ListView.builder(
@@ -90,14 +101,20 @@ class SelectUserForSharingPage extends HookConsumerWidget {
leading: _buildTileIcon(users[index]),
title: Text(
users[index].email,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold),
),
onTap: () {
if (sharedUsersList.value.contains(users[index])) {
sharedUsersList.value =
sharedUsersList.value.where((selectedUser) => selectedUser.id != users[index].id).toSet();
sharedUsersList.value = sharedUsersList.value
.where((selectedUser) =>
selectedUser.id != users[index].id)
.toSet();
} else {
sharedUsersList.value = {...sharedUsersList.value, users[index]};
sharedUsersList.value = {
...sharedUsersList.value,
users[index]
};
}
},
);
@@ -124,7 +141,8 @@ class SelectUserForSharingPage extends HookConsumerWidget {
),
actions: [
TextButton(
onPressed: sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
onPressed:
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
child: const Text(
"Create Album",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),

View File

@@ -29,12 +29,14 @@ class SharingPage extends HookConsumerWidget {
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
String thumbnailUrl = sharedAlbums[index].albumThumbnailAssetId != null
String thumbnailUrl = sharedAlbums[index].albumThumbnailAssetId !=
null
? "$thumbnailRequestUrl/${sharedAlbums[index].albumThumbnailAssetId}"
: "https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60";
return ListTile(
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
contentPadding:
const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: FadeInImage(
@@ -44,7 +46,9 @@ class SharingPage extends HookConsumerWidget {
placeholder: MemoryImage(kTransparentImage),
image: NetworkImage(
thumbnailUrl,
headers: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
headers: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
),
fadeInDuration: const Duration(milliseconds: 200),
fadeOutDuration: const Duration(milliseconds: 200),
@@ -54,10 +58,14 @@ class SharingPage extends HookConsumerWidget {
sharedAlbums[index].albumName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey.shade800),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800),
),
onTap: () {
AutoRouter.of(context).push(AlbumViewerRoute(albumId: sharedAlbums[index].id));
AutoRouter.of(context)
.push(AlbumViewerRoute(albumId: sharedAlbums[index].id));
},
);
},
@@ -134,7 +142,9 @@ class SharingPage extends HookConsumerWidget {
),
),
),
sharedAlbums.isNotEmpty ? _buildAlbumList() : _buildEmptyListIndication()
sharedAlbums.isNotEmpty
? _buildAlbumList()
: _buildEmptyListIndication()
],
),
);

View File

@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
import 'package:immich_mobile/modules/login/views/change_password_page.dart';
import 'package:immich_mobile/modules/login/views/login_page.dart';
import 'package:immich_mobile/modules/home/views/home_page.dart';
import 'package:immich_mobile/modules/search/views/search_page.dart';
@@ -30,6 +31,7 @@ part 'router.gr.dart';
routes: <AutoRoute>[
AutoRoute(page: SplashScreenPage, initial: true),
AutoRoute(page: LoginPage),
AutoRoute(page: ChangePasswordPage),
CustomRoute(
page: TabControllerPage,
guards: [AuthGuard],

View File

@@ -29,6 +29,10 @@ class _$AppRouter extends RootStackRouter {
return MaterialPageX<dynamic>(
routeData: routeData, child: const LoginPage());
},
ChangePasswordRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData, child: const ChangePasswordPage());
},
TabControllerRoute.name: (routeData) {
return CustomPage<dynamic>(
routeData: routeData,
@@ -131,6 +135,7 @@ class _$AppRouter extends RootStackRouter {
List<RouteConfig> get routes => [
RouteConfig(SplashScreenRoute.name, path: '/'),
RouteConfig(LoginRoute.name, path: '/login-page'),
RouteConfig(ChangePasswordRoute.name, path: '/change-password-page'),
RouteConfig(TabControllerRoute.name,
path: '/tab-controller-page',
guards: [
@@ -192,6 +197,15 @@ class LoginRoute extends PageRouteInfo<void> {
static const String name = 'LoginRoute';
}
/// generated route for
/// [ChangePasswordPage]
class ChangePasswordRoute extends PageRouteInfo<void> {
const ChangePasswordRoute()
: super(ChangePasswordRoute.name, path: '/change-password-page');
static const String name = 'ChangePasswordRoute';
}
/// generated route for
/// [TabControllerPage]
class TabControllerRoute extends PageRouteInfo<void> {

View File

@@ -149,7 +149,8 @@ class ImmichExif {
String toJson() => json.encode(toMap());
factory ImmichExif.fromJson(String source) => ImmichExif.fromMap(json.decode(source));
factory ImmichExif.fromJson(String source) =>
ImmichExif.fromMap(json.decode(source));
@override
String toString() {

View File

@@ -95,7 +95,8 @@ class ImmichAsset extends Equatable {
String toJson() => json.encode(toMap());
factory ImmichAsset.fromJson(String source) => ImmichAsset.fromMap(json.decode(source));
factory ImmichAsset.fromJson(String source) =>
ImmichAsset.fromMap(json.decode(source));
@override
String toString() {

View File

@@ -85,13 +85,15 @@ class ImmichAssetWithExif {
originalPath: map['originalPath'] ?? '',
isFavorite: map['isFavorite'] ?? false,
duration: map['duration'],
exifInfo: map['exifInfo'] != null ? ImmichExif.fromMap(map['exifInfo']) : null,
exifInfo:
map['exifInfo'] != null ? ImmichExif.fromMap(map['exifInfo']) : null,
);
}
String toJson() => json.encode(toMap());
factory ImmichAssetWithExif.fromJson(String source) => ImmichAssetWithExif.fromMap(json.decode(source));
factory ImmichAssetWithExif.fromJson(String source) =>
ImmichAssetWithExif.fromMap(json.decode(source));
@override
String toString() {

View File

@@ -34,16 +34,20 @@ class MapboxInfo {
String toJson() => json.encode(toMap());
factory MapboxInfo.fromJson(String source) => MapboxInfo.fromMap(json.decode(source));
factory MapboxInfo.fromJson(String source) =>
MapboxInfo.fromMap(json.decode(source));
@override
String toString() => 'MapboxInfo(isEnable: $isEnable, mapboxSecret: $mapboxSecret)';
String toString() =>
'MapboxInfo(isEnable: $isEnable, mapboxSecret: $mapboxSecret)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is MapboxInfo && other.isEnable == isEnable && other.mapboxSecret == mapboxSecret;
return other is MapboxInfo &&
other.isEnable == isEnable &&
other.mapboxSecret == mapboxSecret;
}
@override

View File

@@ -64,7 +64,8 @@ class ServerInfo {
String toJson() => json.encode(toMap());
factory ServerInfo.fromJson(String source) => ServerInfo.fromMap(json.decode(source));
factory ServerInfo.fromJson(String source) =>
ServerInfo.fromMap(json.decode(source));
@override
String toString() {

View File

@@ -26,7 +26,8 @@ class ServerInfoState {
mapboxInfo: mapboxInfo ?? this.mapboxInfo,
serverVersion: serverVersion ?? this.serverVersion,
isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch,
versionMismatchErrorMessage: versionMismatchErrorMessage ?? this.versionMismatchErrorMessage,
versionMismatchErrorMessage:
versionMismatchErrorMessage ?? this.versionMismatchErrorMessage,
);
}
@@ -50,7 +51,8 @@ class ServerInfoState {
String toJson() => json.encode(toMap());
factory ServerInfoState.fromJson(String source) => ServerInfoState.fromMap(json.decode(source));
factory ServerInfoState.fromJson(String source) =>
ServerInfoState.fromMap(json.decode(source));
@override
String toString() {

View File

@@ -47,7 +47,8 @@ class ServerVersion {
String toJson() => json.encode(toMap());
factory ServerVersion.fromJson(String source) => ServerVersion.fromMap(json.decode(source));
factory ServerVersion.fromJson(String source) =>
ServerVersion.fromMap(json.decode(source));
@override
String toString() {

View File

@@ -36,16 +36,20 @@ class UploadProfileImageResponse {
String toJson() => json.encode(toMap());
factory UploadProfileImageResponse.fromJson(String source) => UploadProfileImageResponse.fromMap(json.decode(source));
factory UploadProfileImageResponse.fromJson(String source) =>
UploadProfileImageResponse.fromMap(json.decode(source));
@override
String toString() => 'UploadProfileImageReponse(userId: $userId, profileImagePath: $profileImagePath)';
String toString() =>
'UploadProfileImageReponse(userId: $userId, profileImagePath: $profileImagePath)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UploadProfileImageResponse && other.userId == userId && other.profileImagePath == profileImagePath;
return other is UploadProfileImageResponse &&
other.userId == userId &&
other.profileImagePath == profileImagePath;
}
@override

Some files were not shown because too many files have changed in this diff Show More