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 push: true
tags: | tags: |
altran1502/immich-web:latest 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' }} push: ${{ github.event_name == 'pull_request' }}
tags: | tags: |
altran1502/immich-web:staging 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' }} push: ${{ github.event_name != 'pull_request' }}
tags: | tags: |
altran1502/immich-server:${{ steps.previoustag.outputs.tag }} altran1502/immich-server:${{ steps.previoustag.outputs.tag }}
altran1502/immich-server:release
build_and_push_machine_learning_release: build_and_push_machine_learning_release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -75,6 +76,7 @@ jobs:
push: true push: true
tags: | tags: |
altran1502/immich-machine-learning:${{ steps.previoustag.outputs.tag }} altran1502/immich-machine-learning:${{ steps.previoustag.outputs.tag }}
altran1502/immich-machine-learning:release
build_and_push_web_release: build_and_push_web_release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -114,3 +116,43 @@ jobs:
target: prod target: prod
tags: | tags: |
altran1502/immich-web:${{ steps.previoustag.outputs.tag }} 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"> <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" /> <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>
<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"/> <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> </a>
<br/> <br/>
@@ -185,7 +185,11 @@ Additional accounts on the server can be created by the admin account.
## Step 4: Run mobile app ## 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 ## F-Droid
You can get the app on F-droid by clicking the image below. 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 context: ../server
dockerfile: Dockerfile dockerfile: Dockerfile
command: npm run start:dev immich command: npm run start:dev immich
expose:
- "3000"
volumes: volumes:
- ../server:/usr/src/app - ../server:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload - ${UPLOAD_LOCATION}:/usr/src/app/upload
@@ -27,8 +25,6 @@ services:
context: ../machine-learning context: ../machine-learning
dockerfile: Dockerfile dockerfile: Dockerfile
command: npm run start:dev command: npm run start:dev
expose:
- "3001"
volumes: volumes:
- ../machine-learning:/usr/src/app - ../machine-learning:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload - ${UPLOAD_LOCATION}:/usr/src/app/upload
@@ -68,7 +64,7 @@ services:
env_file: env_file:
- .env - .env
ports: ports:
- 3002:3000 - 3000:3000
- 24678:24678 - 24678:24678
volumes: volumes:
- ../web:/usr/src/app - ../web:/usr/src/app
@@ -94,11 +90,12 @@ services:
ports: ports:
- 5432:5432 - 5432:5432
nginx: immich-proxy:
container_name: proxy_nginx container_name: immich_proxy
image: nginx:latest image: immich-proxy-dev:latest
volumes: build:
- ./settings/nginx-conf:/etc/nginx/conf.d context: ../nginx
dockerfile: Dockerfile
ports: ports:
- 2283:80 - 2283:80
- 2284:443 - 2284:443
@@ -106,6 +103,7 @@ services:
driver: none driver: none
depends_on: depends_on:
- immich-server - immich-server
restart: always
volumes: volumes:
pgdata: pgdata:

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import { Logger } from '@nestjs/common';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
await app.listen(3001, () => { await app.listen(3003, () => {
if (process.env.NODE_ENV == 'development') { if (process.env.NODE_ENV == 'development') {
Logger.log( Logger.log(
'Running Immich Machine Learning in DEVELOPMENT environment', '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" desc "iOS Beta"
lane :beta do lane :beta do
increment_version_number( increment_version_number(
version_number: "1.13.0" version_number: "1.16.0"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,

View File

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

View File

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

View File

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

View File

@@ -15,81 +15,72 @@ class ExifBottomSheet extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
_buildMap() { _buildMap() {
return (assetDetail.exifInfo!.latitude != null && return Padding(
assetDetail.exifInfo!.longitude != null) padding: const EdgeInsets.symmetric(vertical: 16.0),
? Padding( child: Container(
padding: const EdgeInsets.symmetric(vertical: 16.0), height: 150,
child: Container( width: MediaQuery.of(context).size.width,
height: 150, decoration: const BoxDecoration(
width: MediaQuery.of(context).size.width, borderRadius: BorderRadius.all(Radius.circular(15)),
decoration: const BoxDecoration( ),
borderRadius: BorderRadius.all(Radius.circular(15)), child: FlutterMap(
), options: MapOptions(
child: FlutterMap( center: LatLng(assetDetail.exifInfo!.latitude!,
options: MapOptions( assetDetail.exifInfo!.longitude!),
center: LatLng(assetDetail.exifInfo!.latitude!, zoom: 16.0,
assetDetail.exifInfo!.longitude!), ),
zoom: 16.0, layers: [
), TileLayerOptions(
layers: [ urlTemplate:
TileLayerOptions( "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
urlTemplate: subdomains: ['a', 'b', 'c'],
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", attributionBuilder: (_) {
subdomains: ['a', 'b', 'c'], return const Text(
attributionBuilder: (_) { "© OpenStreetMap",
return const Text( style: TextStyle(fontSize: 10),
"© OpenStreetMap", );
style: TextStyle(fontSize: 10), },
);
},
),
MarkerLayerOptions(
markers: [
Marker(
anchorPos: AnchorPos.align(AnchorAlign.top),
point: LatLng(assetDetail.exifInfo!.latitude!,
assetDetail.exifInfo!.longitude!),
builder: (ctx) => const Image(
image: AssetImage('assets/location-pin.png')),
),
],
),
],
),
), ),
) MarkerLayerOptions(
: Container(); markers: [
Marker(
anchorPos: AnchorPos.align(AnchorAlign.top),
point: LatLng(assetDetail.exifInfo!.latitude!,
assetDetail.exifInfo!.longitude!),
builder: (ctx) => const Image(
image: AssetImage('assets/location-pin.png')),
),
],
),
],
),
),
);
} }
_buildLocationText() { _buildLocationText() {
return (assetDetail.exifInfo!.city != null && return Text(
assetDetail.exifInfo!.state != null) "${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
? Text( style: TextStyle(
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}", fontSize: 12, color: Colors.grey[200], fontWeight: FontWeight.bold),
style: TextStyle( );
fontSize: 12,
color: Colors.grey[200],
fontWeight: FontWeight.bold),
)
: Container();
} }
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8), padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
child: ListView( child: ListView(
children: [ children: [
assetDetail.exifInfo?.dateTimeOriginal != null if (assetDetail.exifInfo?.dateTimeOriginal != null)
? Text( Text(
DateFormat('E, LLL d, y • h:mm a').format( DateFormat('E, LLL d, y • h:mm a').format(
DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!), DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!),
), ),
style: TextStyle( style: TextStyle(
color: Colors.grey[400], color: Colors.grey[400],
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
), ),
) ),
: Container(),
Padding( Padding(
padding: const EdgeInsets.only(top: 16.0), padding: const EdgeInsets.only(top: 16.0),
child: Text( child: Text(
@@ -102,84 +93,83 @@ class ExifBottomSheet extends ConsumerWidget {
), ),
// Location // Location
assetDetail.exifInfo?.latitude != null if (assetDetail.exifInfo?.latitude != null)
? Padding( Padding(
padding: const EdgeInsets.only(top: 32.0), padding: const EdgeInsets.only(top: 32.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Divider( Divider(
thickness: 1, thickness: 1,
color: Colors.grey[600], color: Colors.grey[600],
),
Text(
"LOCATION",
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
),
_buildMap(),
_buildLocationText(),
Text(
"${assetDetail.exifInfo!.latitude!.toStringAsFixed(4)}, ${assetDetail.exifInfo!.longitude!.toStringAsFixed(4)}",
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
)
],
), ),
) Text(
: Container(), "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)}",
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
)
],
),
),
// Detail // Detail
assetDetail.exifInfo != null if (assetDetail.exifInfo != null)
? Padding( Padding(
padding: const EdgeInsets.only(top: 32.0), padding: const EdgeInsets.only(top: 32.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Divider( Divider(
thickness: 1, thickness: 1,
color: Colors.grey[600], color: Colors.grey[600],
),
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"DETAILS",
style:
TextStyle(fontSize: 11, color: Colors.grey[400]),
),
),
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
textColor: Colors.grey[300],
iconColor: Colors.grey[300],
leading: const Icon(Icons.image),
title: Text(
"${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: assetDetail.exifInfo?.exifImageHeight != null
? Text(
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ")
: Container(),
),
assetDetail.exifInfo?.make != null
? ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
textColor: Colors.grey[300],
iconColor: Colors.grey[300],
leading: const Icon(Icons.camera),
title: Text(
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
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()
],
), ),
) Padding(
: Container() padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"DETAILS",
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
),
),
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
textColor: Colors.grey[300],
iconColor: Colors.grey[300],
leading: const Icon(Icons.image),
title: Text(
"${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: assetDetail.exifInfo?.exifImageHeight != null
? Text(
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ")
: null,
),
if (assetDetail.exifInfo?.make != null)
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
textColor: Colors.grey[300],
iconColor: Colors.grey[300],
leading: const Icon(Icons.camera),
title: Text(
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} "),
),
],
),
),
], ],
), ),
); );

View File

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

View File

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

View File

@@ -21,13 +21,16 @@ class AvailableAlbum {
} }
@override @override
String toString() => 'AvailableAlbum(albumEntity: $albumEntity, thumbnailData: $thumbnailData)'; String toString() =>
'AvailableAlbum(albumEntity: $albumEntity, thumbnailData: $thumbnailData)';
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; 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 @override

View File

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

View File

@@ -19,7 +19,8 @@ class HiveBackupAlbums {
}); });
@override @override
String toString() => 'HiveBackupAlbums(selectedAlbumIds: $selectedAlbumIds, excludedAlbumsIds: $excludedAlbumsIds)'; String toString() =>
'HiveBackupAlbums(selectedAlbumIds: $selectedAlbumIds, excludedAlbumsIds: $excludedAlbumsIds)';
HiveBackupAlbums copyWith({ HiveBackupAlbums copyWith({
List<String>? selectedAlbumIds, List<String>? selectedAlbumIds,
@@ -49,7 +50,8 @@ class HiveBackupAlbums {
String toJson() => json.encode(toMap()); 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 @override
bool operator ==(Object other) { 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/backup_state.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/services/backup.service.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/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/models/server_info.model.dart'; import 'package:immich_mobile/shared/models/server_info.model.dart';
import 'package:immich_mobile/shared/services/server_info.service.dart'; import 'package:immich_mobile/shared/services/server_info.service.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
class BackupNotifier extends StateNotifier<BackUpState> { class BackupNotifier extends StateNotifier<BackUpState> {
BackupNotifier({this.ref}) BackupNotifier(this._backupService, this._serverInfoService, this._authState)
: super( : super(
BackUpState( BackUpState(
backupProgress: BackUpProgressEnum.idle, backupProgress: BackUpProgressEnum.idle,
allAssetOnDatabase: const [], allAssetsInDatabase: const [],
progressInPercentage: 0, progressInPercentage: 0,
cancelToken: CancellationToken(), cancelToken: CancellationToken(),
serverInfo: ServerInfo( serverInfo: ServerInfo(
@@ -35,11 +36,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
allUniqueAssets: const {}, allUniqueAssets: const {},
selectedAlbumsBackupAssetsIds: const {}, selectedAlbumsBackupAssetsIds: const {},
), ),
); ) {
getBackupInfo();
}
Ref? ref; final BackupService _backupService;
final BackupService _backupService = BackupService(); final ServerInfoService _serverInfoService;
final ServerInfoService _serverInfoService = ServerInfoService(); final AuthenticationState _authState;
/// ///
/// UI INTERACTION /// 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 /// 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) /// 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 // Get all albums on the device
List<AvailableAlbum> availableAlbums = []; List<AvailableAlbum> availableAlbums = [];
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList( 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 /// Find the assets that are not overlapping between the two sets
/// Those assets are unique and are used as the total assets /// Those assets are unique and are used as the total assets
/// ///
void _updateBackupAssetCount() async { Future<void> _updateBackupAssetCount() async {
Set<AssetEntity> assetsFromSelectedAlbums = {}; Set<AssetEntity> assetsFromSelectedAlbums = {};
Set<AssetEntity> assetsFromExcludedAlbums = {}; Set<AssetEntity> assetsFromExcludedAlbums = {};
@@ -194,27 +197,27 @@ class BackupNotifier extends StateNotifier<BackUpState> {
Set<AssetEntity> allUniqueAssets = Set<AssetEntity> allUniqueAssets =
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
List<String> allAssetOnDatabase = List<String> allAssetsInDatabase =
await _backupService.getDeviceBackupAsset(); await _backupService.getDeviceBackupAsset();
// Find asset that were backup from selected albums // Find asset that were backup from selected albums
Set<String> selectedAlbumsBackupAssets = Set<String> selectedAlbumsBackupAssets =
Set.from(allUniqueAssets.map((e) => e.id)); Set.from(allUniqueAssets.map((e) => e.id));
selectedAlbumsBackupAssets selectedAlbumsBackupAssets
.removeWhere((assetId) => !allAssetOnDatabase.contains(assetId)); .removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
if (allUniqueAssets.isEmpty) { if (allUniqueAssets.isEmpty) {
debugPrint("No Asset On Device"); debugPrint("No Asset On Device");
state = state.copyWith( state = state.copyWith(
backupProgress: BackUpProgressEnum.idle, backupProgress: BackUpProgressEnum.idle,
allAssetOnDatabase: allAssetOnDatabase, allAssetsInDatabase: allAssetsInDatabase,
allUniqueAssets: {}, allUniqueAssets: {},
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
); );
return; return;
} else { } else {
state = state.copyWith( state = state.copyWith(
allAssetOnDatabase: allAssetOnDatabase, allAssetsInDatabase: allAssetsInDatabase,
allUniqueAssets: allUniqueAssets, allUniqueAssets: allUniqueAssets,
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
); );
@@ -222,6 +225,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
// Save to persistent storage // Save to persistent storage
_updatePersistentAlbumsSelection(); _updatePersistentAlbumsSelection();
return;
} }
/// ///
@@ -229,10 +234,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// which albums are selected or excluded /// which albums are selected or excluded
/// and then update the UI according to those information /// and then update the UI according to those information
/// ///
void getBackupInfo() async { Future<void> getBackupInfo() async {
await getBackupAlbumsInfo(); await _getBackupAlbumsInfo();
_updateServerInfo(); await _updateServerInfo();
_updateBackupAssetCount(); await _updateBackupAssetCount();
} }
/// ///
@@ -255,11 +260,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// Invoke backup process /// Invoke backup process
/// ///
void startBackupProcess() async { void startBackupProcess() async {
_updateServerInfo();
_updateBackupAssetCount();
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
await getBackupInfo();
var authResult = await PhotoManager.requestPermissionExtend(); var authResult = await PhotoManager.requestPermissionExtend();
if (authResult.isAuth) { if (authResult.isAuth) {
await PhotoManager.clearFileCache(); await PhotoManager.clearFileCache();
@@ -270,10 +274,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
return; return;
} }
Set<AssetEntity> assetsWillBeBackup = state.allUniqueAssets; Set<AssetEntity> assetsWillBeBackup = Set.from(state.allUniqueAssets);
// Remove item that has already been backed up // 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); assetsWillBeBackup.removeWhere((e) => e.id == assetId);
} }
@@ -300,8 +304,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state = state.copyWith(selectedAlbumsBackupAssetsIds: { state = state.copyWith(selectedAlbumsBackupAssetsIds: {
...state.selectedAlbumsBackupAssetsIds, ...state.selectedAlbumsBackupAssetsIds,
deviceAssetId deviceAssetId
}, allAssetOnDatabase: [ }, allAssetsInDatabase: [
...state.allAssetOnDatabase, ...state.allAssetsInDatabase,
deviceAssetId deviceAssetId
]); ]);
@@ -320,7 +324,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
progressInPercentage: (sent.toDouble() / total.toDouble() * 100)); progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
} }
void _updateServerInfo() async { Future<void> _updateServerInfo() async {
var serverInfo = await _serverInfoService.getServerInfo(); var serverInfo = await _serverInfoService.getServerInfo();
// Update server info // Update server info
@@ -338,38 +342,38 @@ class BackupNotifier extends StateNotifier<BackUpState> {
} }
void resumeBackup() { void resumeBackup() {
var authState = ref?.read(authenticationProvider);
// Check if user is login // Check if user is login
var accessKey = Hive.box(userInfoBox).get(accessTokenKey); var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
// User has been logged out return // User has been logged out return
if (authState != null) { if (accessKey == null || !_authState.isAuthenticated) {
if (accessKey == null || !authState.isAuthenticated) { debugPrint("[resumeBackup] not authenticated - abort");
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) {
// check if backup is alreayd in process - then return
if (state.backupProgress == BackUpProgressEnum.inProgress) {
debugPrint("[resumeBackup] Backup is already in progress - abort");
return; return;
} }
// Check if this device is enable backup by the user // Run backup
if ((authState.deviceInfo.deviceId == authState.deviceId) && debugPrint("[resumeBackup] Start back up");
authState.deviceInfo.isAutoBackup) { startBackupProcess();
// check if backup is alreayd in process - then return
if (state.backupProgress == BackUpProgressEnum.inProgress) {
debugPrint("[resumeBackup] Backup is already in progress - abort");
return;
}
// Run backup
debugPrint("[resumeBackup] Start back up");
startBackupProcess();
}
return;
} }
return;
} }
} }
final backupProvider = final backupProvider =
StateNotifierProvider<BackupNotifier, BackUpState>((ref) { 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:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.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/constants/hive_box.dart';
import 'package:immich_mobile/shared/services/network.service.dart'; import 'package:immich_mobile/shared/services/network.service.dart';
import 'package:immich_mobile/shared/models/device_info.model.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:path/path.dart' as p;
import 'package:cancellation_token_http/http.dart' as http; import 'package:cancellation_token_http/http.dart' as http;
final backupServiceProvider =
Provider((ref) => BackupService(ref.watch(networkServiceProvider)));
class BackupService { class BackupService {
final NetworkService _networkService = NetworkService(); final NetworkService _networkService;
BackupService(this._networkService);
Future<List<String>> getDeviceBackupAsset() async { Future<List<String>> getDeviceBackupAsset() async {
String deviceId = Hive.box(userInfoBox).get(deviceIdKey); 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()); List<dynamic> result = jsonDecode(response.toString());
return result.cast<String>(); return result.cast<String>();
} }
backupAsset(Set<AssetEntity> assetList, http.CancellationToken cancelToken, backupAsset(
Function(String, String) singleAssetDoneCb, Function(int, int) uploadProgress) async { Set<AssetEntity> assetList,
http.CancellationToken cancelToken,
Function(String, String) singleAssetDoneCb,
Function(int, int) uploadProgress) async {
String deviceId = Hive.box(userInfoBox).get(deviceIdKey); String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
File? file; File? file;
@@ -44,7 +53,8 @@ class BackupService {
if (file != null) { if (file != null) {
String originalFileName = await entity.titleAsync; String originalFileName = await entity.titleAsync;
String fileNameWithoutPath = originalFileName.toString().split(".")[0]; String fileNameWithoutPath =
originalFileName.toString().split(".")[0];
var fileExtension = p.extension(file.path); var fileExtension = p.extension(file.path);
var mimeType = FileHelper.getMimeType(file.path); var mimeType = FileHelper.getMimeType(file.path);
var fileStream = file.openRead(); 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 box = Hive.box(userInfoBox);
var req = MultipartRequest('POST', Uri.parse('$savedEndpoint/asset/upload'), var req = MultipartRequest(
onProgress: ((bytes, totalBytes) => uploadProgress(bytes, totalBytes))); 'POST', Uri.parse('$savedEndpoint/asset/upload'),
onProgress: ((bytes, totalBytes) =>
uploadProgress(bytes, totalBytes)));
req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}"; req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}";
req.fields['deviceAssetId'] = entity.id; req.fields['deviceAssetId'] = entity.id;
@@ -88,9 +86,6 @@ class BackupService {
req.fields['fileExtension'] = fileExtension; req.fields['fileExtension'] = fileExtension;
req.fields['duration'] = entity.videoDuration.toString(); req.fields['duration'] = entity.videoDuration.toString();
if (thumbnailUploadData != null) {
req.files.add(thumbnailUploadData);
}
req.files.add(assetRawUploadData); req.files.add(assetRawUploadData);
var res = await req.send(cancellationToken: cancelToken); 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: { var res = await _networkService.patchRequest(url: 'device-info', data: {
"isAutoBackup": status, "isAutoBackup": status,
"deviceId": deviceId, "deviceId": deviceId,

View File

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

View File

@@ -4,7 +4,12 @@ class BackupInfoCard extends StatelessWidget {
final String title; final String title;
final String subtitle; final String subtitle;
final String info; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

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

View File

@@ -17,7 +17,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
useEffect(() { useEffect(() {
ref.read(backupProvider.notifier).getBackupAlbumsInfo(); ref.read(backupProvider.notifier).getBackupInfo();
return null; return null;
}, []); }, []);
@@ -37,8 +37,12 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
itemBuilder: ((context, index) { itemBuilder: ((context, index) {
var thumbnailData = availableAlbums[index].thumbnailData; var thumbnailData = availableAlbums[index].thumbnailData;
return Padding( return Padding(
padding: index == 0 ? const EdgeInsets.only(left: 16.00) : const EdgeInsets.all(0), padding: index == 0
child: AlbumInfoCard(imageData: thumbnailData, albumInfo: availableAlbums[index].albumEntity), ? 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, onTap: removeSelection,
child: Chip( child: Chip(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
label: Text( label: Text(
album.name, 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, backgroundColor: Theme.of(context).primaryColor,
deleteIconColor: Colors.white, deleteIconColor: Colors.white,
@@ -88,7 +96,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
_buildExcludedAlbumNameChip() { _buildExcludedAlbumNameChip() {
return excludedBackupAlbums.map((album) { return excludedBackupAlbums.map((album) {
void removeSelection() { void removeSelection() {
ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(album); ref
.watch(backupProvider.notifier)
.removeExcludedAlbumForBackup(album);
} }
return GestureDetector( return GestureDetector(
@@ -97,10 +107,14 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: Chip( child: Chip(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
label: Text( label: Text(
album.name, 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], backgroundColor: Colors.red[300],
deleteIconColor: Colors.white, deleteIconColor: Colors.white,
@@ -142,7 +156,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap( child: Wrap(
children: [..._buildSelectedAlbumNameChip(), ..._buildExcludedAlbumNameChip()], children: [
..._buildSelectedAlbumNameChip(),
..._buildExcludedAlbumNameChip()
],
), ),
), ),
@@ -165,10 +182,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
title: Text( title: Text(
"Total unique assets", "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( trailing: Text(
ref.watch(backupProvider).allUniqueAssets.length.toString(), '${ref.watch(backupProvider).allUniqueAssets.length}',
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
), ),
@@ -179,7 +199,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
ListTile( ListTile(
title: Text( title: Text(
"Albums on device (${availableAlbums.length.toString()})", "Albums on device (${availableAlbums.length})",
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
), ),
subtitle: Padding( subtitle: Padding(
@@ -206,7 +226,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
elevation: 5, elevation: 5,
title: Text( title: Text(
'Selection Info', 'Selection Info',
@@ -221,7 +242,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
children: [ children: [
Text( Text(
'Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.', '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(() { useEffect(() {
if (backupState.backupProgress != BackUpProgressEnum.inProgress) { if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
ref.read(backupProvider.notifier).getBackupInfo(); ref.watch(backupProvider.notifier).getBackupInfo();
} }
ref ref
@@ -96,12 +96,11 @@ class BackupControllerPage extends HookConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
!isAutoBackup if (!isAutoBackup)
? const Text( const Text(
"Turn on backup to automatically upload new assets to the server.", "Turn on backup to automatically upload new assets to the server.",
style: TextStyle(fontSize: 14), style: TextStyle(fontSize: 14),
) ),
: Container(),
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: OutlinedButton( child: OutlinedButton(
@@ -112,13 +111,15 @@ class BackupControllerPage extends HookConsumerWidget {
), ),
), ),
onPressed: () { onPressed: () {
isAutoBackup if (isAutoBackup) {
? ref ref
.watch(authenticationProvider.notifier) .read(authenticationProvider.notifier)
.setAutoBackup(false) .setAutoBackup(false);
: ref } else {
.watch(authenticationProvider.notifier) ref
.setAutoBackup(true); .read(authenticationProvider.notifier)
.setAutoBackup(true);
}
}, },
child: Text("Turn $backupBtnText Backup", child: Text("Turn $backupBtnText Backup",
style: const TextStyle(fontWeight: FontWeight.bold)), style: const TextStyle(fontWeight: FontWeight.bold)),
@@ -187,7 +188,7 @@ class BackupControllerPage extends HookConsumerWidget {
), ),
); );
} else { } else {
return Container(); return const SizedBox();
} }
} }
@@ -212,7 +213,7 @@ class BackupControllerPage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( const Text(
"Albums to be backup", "Albums to be backed up",
style: TextStyle(color: Color(0xFF808080), fontSize: 12), style: TextStyle(color: Color(0xFF808080), fontSize: 12),
), ),
_buildSelectedAlbumName(), _buildSelectedAlbumName(),
@@ -282,14 +283,12 @@ class BackupControllerPage extends HookConsumerWidget {
), ),
BackupInfoCard( BackupInfoCard(
title: "Backup", title: "Backup",
subtitle: subtitle: "Backed up photos and videos",
"Photos and videos from selected albums that are backup",
info: "${backupState.selectedAlbumsBackupAssetsIds.length}", info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
), ),
BackupInfoCard( BackupInfoCard(
title: "Remainder", title: "Remainder",
subtitle: subtitle: "Remaining photos and albums to back up from selection",
"Photos and videos that has not been backing up from selected albums",
info: info:
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}", "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
), ),

View File

@@ -35,7 +35,8 @@ class DeleteAssetResponse {
String toJson() => json.encode(toMap()); 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 @override
String toString() => 'DeleteAssetResponse(id: $id, status: $status)'; String toString() => 'DeleteAssetResponse(id: $id, status: $status)';
@@ -44,7 +45,9 @@ class DeleteAssetResponse {
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; 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 @override

View File

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

View File

@@ -37,14 +37,16 @@ class HomePageState {
factory HomePageState.fromMap(Map<String, dynamic> map) { factory HomePageState.fromMap(Map<String, dynamic> map) {
return HomePageState( return HomePageState(
isMultiSelectEnable: map['isMultiSelectEnable'] ?? false, 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']), selectedDateGroup: Set<String>.from(map['selectedDateGroup']),
); );
} }
String toJson() => json.encode(toMap()); 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 @override
String toString() => String toString() =>
@@ -62,5 +64,8 @@ class HomePageState {
} }
@override @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) { void addSelectedDateGroup(String dateGroupTitle) {
state = state.copyWith(selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle}); state = state.copyWith(
selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle});
} }
void removeSelectedDateGroup(String dateGroupTitle) { void removeSelectedDateGroup(String dateGroupTitle) {
@@ -25,11 +26,13 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
} }
void enableMultiSelect(Set<ImmichAsset> selectedItems) { void enableMultiSelect(Set<ImmichAsset> selectedItems) {
state = state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems); state =
state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
} }
void disableMultiSelect() { void disableMultiSelect() {
state = state.copyWith(isMultiSelectEnable: false, selectedItems: {}, selectedDateGroup: {}); state = state.copyWith(
isMultiSelectEnable: false, selectedItems: {}, selectedDateGroup: {});
} }
void addSingleSelectedItem(ImmichAsset asset) { void addSingleSelectedItem(ImmichAsset asset) {
@@ -60,4 +63,5 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
} }
final homePageStateProvider = 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()); 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 @override
String toString() => 'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)'; String toString() =>
'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)';
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; 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 @override
int get hashCode => status.hashCode ^ profileImagePath.hashCode; int get hashCode => status.hashCode ^ profileImagePath.hashCode;
} }
class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState> { class UploadProfileImageNotifier
UploadProfileImageNotifier() extends StateNotifier<UploadProfileImageState> {
UploadProfileImageNotifier(this._userSErvice)
: super(UploadProfileImageState( : super(UploadProfileImageState(
profileImagePath: '', profileImagePath: '',
status: UploadProfileStatus.idle, status: UploadProfileStatus.idle,
)); ));
final UserService _userSErvice;
Future<bool> upload(XFile file) async { Future<bool> upload(XFile file) async {
state = state.copyWith(status: UploadProfileStatus.loading); state = state.copyWith(status: UploadProfileStatus.loading);
var res = await UserService().uploadProfileImage(file); var res = await _userSErvice.uploadProfileImage(file);
if (res != null) { if (res != null) {
debugPrint("Succesfully upload profile image"); 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; return true;
} }
@@ -90,4 +99,5 @@ class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState>
} }
final uploadProfileImageProvider = 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 'dart:convert';
import 'package:flutter/material.dart'; 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/delete_asset_response.model.dart';
import 'package:immich_mobile/modules/home/models/get_all_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.model.dart';
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart'; import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
import 'package:immich_mobile/shared/services/network.service.dart'; import 'package:immich_mobile/shared/services/network.service.dart';
final assetServiceProvider =
Provider((ref) => AssetService(ref.watch(networkServiceProvider)));
class AssetService { class AssetService {
final NetworkService _networkService = NetworkService(); final NetworkService _networkService;
AssetService(this._networkService);
Future<List<ImmichAsset>?> getAllAsset() async { Future<List<ImmichAsset>?> getAllAsset() async {
var res = await _networkService.getRequest(url: "asset/"); var res = await _networkService.getRequest(url: "asset/");
try { try {
List<dynamic> decodedData = jsonDecode(res.toString()); 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; return result;
} catch (e) { } catch (e) {
debugPrint("Error getAllAsset ${e.toString()}"); debugPrint("Error getAllAsset ${e.toString()}");
@@ -62,7 +68,8 @@ class AssetService {
List<dynamic> decodedData = jsonDecode(res.toString()); 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) { if (result.isNotEmpty) {
return result; 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 { try {
var payload = []; var payload = [];
@@ -98,11 +106,13 @@ class AssetService {
payload.add(asset.id); 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<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; return result;
} catch (e) { } catch (e) {

View File

@@ -13,7 +13,8 @@ class ControlBottomAppBar extends StatelessWidget {
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height * 0.15, height: MediaQuery.of(context).size.height * 0.15,
decoration: BoxDecoration( 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), color: Colors.grey[300]?.withOpacity(0.98),
), ),
child: Column( child: Column(
@@ -46,7 +47,11 @@ class ControlBottomAppBar extends StatelessWidget {
} }
class ControlBoxButton 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); : super(key: key);
final String label; final String label;

View File

@@ -18,9 +18,12 @@ class DailyTitleText extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
var currentYear = DateTime.now().year; var currentYear = DateTime.now().year;
var groupYear = DateTime.parse(isoDate).year; var groupYear = DateTime.parse(isoDate).year;
var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy'; var formatDateTemplate =
var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(isoDate)); currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable; var dateText =
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable;
var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup; var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
var selectedItems = ref.watch(homePageStateProvider).selectedItems; var selectedItems = ref.watch(homePageStateProvider).selectedItems;
@@ -35,23 +38,42 @@ class DailyTitleText extends ConsumerWidget {
selectedDateGroup.contains(dateText) && selectedDateGroup.contains(dateText) &&
selectedItems.length != assetGroup.length) { 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 // 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
ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup); .watch(homePageStateProvider.notifier)
} else if (isMultiSelectEnable && selectedDateGroup.contains(dateText) && selectedDateGroup.length > 1) { .removeSelectedDateGroup(dateText);
ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText); ref
ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup); .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)) { } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText); ref
ref.watch(homePageStateProvider.notifier).addMultipleSelectedItems(assetGroup); .watch(homePageStateProvider.notifier)
.addSelectedDateGroup(dateText);
ref
.watch(homePageStateProvider.notifier)
.addMultipleSelectedItems(assetGroup);
} else { } else {
ref.watch(homePageStateProvider.notifier).enableMultiSelect(assetGroup.toSet()); ref
ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText); .watch(homePageStateProvider.notifier)
.enableMultiSelect(assetGroup.toSet());
ref
.watch(homePageStateProvider.notifier)
.addSelectedDateGroup(dateText);
} }
} }
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Padding( 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( child: Row(
children: [ children: [
Text( Text(

View File

@@ -14,7 +14,8 @@ class DeleteDialog extends ConsumerWidget {
backgroundColor: Colors.grey[200], backgroundColor: Colors.grey[200],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
title: const Text("Delete Permanently"), 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: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -27,7 +28,9 @@ class DeleteDialog extends ConsumerWidget {
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
ref.watch(assetProvider.notifier).deleteAssets(homePageState.selectedItems); ref
.watch(assetProvider.notifier)
.deleteAssets(homePageState.selectedItems);
ref.watch(homePageStateProvider.notifier).disableMultiSelect(); ref.watch(homePageStateProvider.notifier).disableMultiSelect();
Navigator.of(context).pop(); Navigator.of(context).pop();

View File

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

View File

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

View File

@@ -25,27 +25,26 @@ class ImageGrid extends ConsumerWidget {
child: Stack( child: Stack(
children: [ children: [
ThumbnailImage(asset: assetGroup[index]), ThumbnailImage(asset: assetGroup[index]),
assetType == 'IMAGE' if (assetType != 'IMAGE')
? Container() Positioned(
: Positioned( top: 5,
top: 5, right: 5,
right: 5, child: Row(
child: Row( children: [
children: [ Text(
Text( assetGroup[index].duration.toString().substring(0, 7),
assetGroup[index].duration.toString().substring(0, 7), style: const TextStyle(
style: const TextStyle( color: Colors.white,
color: Colors.white, fontSize: 10,
fontSize: 10, ),
),
),
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
), ),
) const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
),
),
], ],
), ),
); );

View File

@@ -49,30 +49,29 @@ class ImmichSliverAppBar extends ConsumerWidget {
}, },
), ),
), ),
serverInfoState.isVersionMismatch if (serverInfoState.isVersionMismatch)
? Positioned( Positioned(
bottom: 12, bottom: 12,
right: 12, right: 12,
child: GestureDetector( child: GestureDetector(
onTap: () => Scaffold.of(context).openDrawer(), onTap: () => Scaffold.of(context).openDrawer(),
child: Material( child: Material(
color: Colors.grey[200], color: Colors.grey[200],
elevation: 1, elevation: 1,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50.0), borderRadius: BorderRadius.circular(50.0),
), ),
child: const Padding( child: const Padding(
padding: EdgeInsets.all(2.0), padding: EdgeInsets.all(2.0),
child: Icon( child: Icon(
Icons.info, Icons.info,
color: Color.fromARGB(255, 243, 188, 106), color: Color.fromARGB(255, 243, 188, 106),
size: 15, size: 15,
),
),
), ),
), ),
) ),
: Container(), ),
),
], ],
); );
}, },
@@ -90,21 +89,20 @@ class ImmichSliverAppBar extends ConsumerWidget {
Stack( Stack(
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
children: [ children: [
backupState.backupProgress == BackUpProgressEnum.inProgress if (backupState.backupProgress == BackUpProgressEnum.inProgress)
? Positioned( Positioned(
top: 10, top: 10,
right: 12, right: 12,
child: SizedBox( child: SizedBox(
height: 8, height: 8,
width: 8, width: 8,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 1, strokeWidth: 1,
valueColor: AlwaysStoppedAnimation<Color>( valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).primaryColor), Theme.of(context).primaryColor),
), ),
), ),
) ),
: Container(),
IconButton( IconButton(
splashRadius: 25, splashRadius: 25,
iconSize: 30, iconSize: 30,
@@ -129,18 +127,15 @@ class ImmichSliverAppBar extends ConsumerWidget {
} }
}, },
), ),
backupState.backupProgress == BackUpProgressEnum.inProgress if (backupState.backupProgress == BackUpProgressEnum.inProgress)
? Positioned( Positioned(
bottom: 5, bottom: 5,
child: Text( child: Text(
(backupState.allUniqueAssets.length - '${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}',
backupState.selectedAlbumsBackupAssetsIds.length) style:
.toString(), const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
style: const TextStyle( ),
fontSize: 9, fontWeight: FontWeight.bold), ),
),
)
: Container()
], ],
), ),
], ],

View File

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

View File

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

View File

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

View File

@@ -16,5 +16,9 @@ class HiveSavedLoginInfo {
@HiveField(3) @HiveField(3)
bool isSaveLogin; 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 lastName;
final String profileImagePath; final String profileImagePath;
final bool isAdmin; final bool isAdmin;
final bool isFirstLogin; final bool shouldChangePassword;
LogInReponse({ LogInReponse({
required this.accessToken, required this.accessToken,
@@ -18,7 +18,7 @@ class LogInReponse {
required this.lastName, required this.lastName,
required this.profileImagePath, required this.profileImagePath,
required this.isAdmin, required this.isAdmin,
required this.isFirstLogin, required this.shouldChangePassword,
}); });
LogInReponse copyWith({ LogInReponse copyWith({
@@ -29,7 +29,7 @@ class LogInReponse {
String? lastName, String? lastName,
String? profileImagePath, String? profileImagePath,
bool? isAdmin, bool? isAdmin,
bool? isFirstLogin, bool? shouldChangePassword,
}) { }) {
return LogInReponse( return LogInReponse(
accessToken: accessToken ?? this.accessToken, accessToken: accessToken ?? this.accessToken,
@@ -39,7 +39,7 @@ class LogInReponse {
lastName: lastName ?? this.lastName, lastName: lastName ?? this.lastName,
profileImagePath: profileImagePath ?? this.profileImagePath, profileImagePath: profileImagePath ?? this.profileImagePath,
isAdmin: isAdmin ?? this.isAdmin, isAdmin: isAdmin ?? this.isAdmin,
isFirstLogin: isFirstLogin ?? this.isFirstLogin, shouldChangePassword: shouldChangePassword ?? this.shouldChangePassword,
); );
} }
@@ -53,7 +53,7 @@ class LogInReponse {
result.addAll({'lastName': lastName}); result.addAll({'lastName': lastName});
result.addAll({'profileImagePath': profileImagePath}); result.addAll({'profileImagePath': profileImagePath});
result.addAll({'isAdmin': isAdmin}); result.addAll({'isAdmin': isAdmin});
result.addAll({'isFirstLogin': isFirstLogin}); result.addAll({'shouldChangePassword': shouldChangePassword});
return result; return result;
} }
@@ -67,17 +67,18 @@ class LogInReponse {
lastName: map['lastName'] ?? '', lastName: map['lastName'] ?? '',
profileImagePath: map['profileImagePath'] ?? '', profileImagePath: map['profileImagePath'] ?? '',
isAdmin: map['isAdmin'] ?? false, isAdmin: map['isAdmin'] ?? false,
isFirstLogin: map['isFirstLogin'] ?? false, shouldChangePassword: map['shouldChangePassword'] ?? false,
); );
} }
String toJson() => json.encode(toMap()); 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 @override
String toString() { 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 @override
@@ -92,7 +93,7 @@ class LogInReponse {
other.lastName == lastName && other.lastName == lastName &&
other.profileImagePath == profileImagePath && other.profileImagePath == profileImagePath &&
other.isAdmin == isAdmin && other.isAdmin == isAdmin &&
other.isFirstLogin == isFirstLogin; other.shouldChangePassword == shouldChangePassword;
} }
@override @override
@@ -104,6 +105,6 @@ class LogInReponse {
lastName.hashCode ^ lastName.hashCode ^
profileImagePath.hashCode ^ profileImagePath.hashCode ^
isAdmin.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'; import 'package:immich_mobile/shared/models/device_info.model.dart';
class AuthenticationNotifier extends StateNotifier<AuthenticationState> { class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
AuthenticationNotifier(this.ref) AuthenticationNotifier(
this._deviceInfoService, this._backupService, this._networkService)
: super( : super(
AuthenticationState( AuthenticationState(
deviceId: "", deviceId: "",
@@ -23,7 +24,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
lastName: '', lastName: '',
profileImagePath: '', profileImagePath: '',
isAdmin: false, isAdmin: false,
isFirstLogin: false, shouldChangePassword: false,
isAuthenticated: false, isAuthenticated: false,
deviceInfo: DeviceInfoRemote( deviceInfo: DeviceInfoRemote(
id: 0, id: 0,
@@ -37,12 +38,12 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
), ),
); );
final Ref ref; final DeviceInfoService _deviceInfoService;
final DeviceInfoService _deviceInfoService = DeviceInfoService(); final BackupService _backupService;
final BackupService _backupService = BackupService(); final NetworkService _networkService;
final NetworkService _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 // Store server endpoint to Hive and test endpoint
if (serverEndpoint[serverEndpoint.length - 1] == "/") { if (serverEndpoint[serverEndpoint.length - 1] == "/") {
var validUrl = serverEndpoint.substring(0, serverEndpoint.length - 1); var validUrl = serverEndpoint.substring(0, serverEndpoint.length - 1);
@@ -71,7 +72,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
// Make sign-in request // Make sign-in request
try { 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()); var payload = LogInReponse.fromJson(res.toString());
@@ -85,7 +87,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
lastName: payload.lastName, lastName: payload.lastName,
profileImagePath: payload.profileImagePath, profileImagePath: payload.profileImagePath,
isAdmin: payload.isAdmin, isAdmin: payload.isAdmin,
isFirstLoggedIn: payload.isFirstLogin, shouldChangePassword: payload.shouldChangePassword,
); );
if (isSavedLoginInfo) { if (isSavedLoginInfo) {
@@ -99,7 +101,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
serverUrl: Hive.box(userInfoBox).get(serverEndpointKey)), serverUrl: Hive.box(userInfoBox).get(serverEndpointKey)),
); );
} else { } else {
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey); Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox)
.delete(savedLoginInfoKey);
} }
} catch (e) { } catch (e) {
return false; return false;
@@ -107,8 +110,13 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
// Register device info // Register device info
try { try {
Response res = await _networkService Response res = await _networkService.postRequest(
.postRequest(url: 'device-info', data: {'deviceId': state.deviceId, 'deviceType': state.deviceType}); url: 'device-info',
data: {
'deviceId': state.deviceId,
'deviceType': state.deviceType,
},
);
DeviceInfoRemote deviceInfo = DeviceInfoRemote.fromJson(res.toString()); DeviceInfoRemote deviceInfo = DeviceInfoRemote.fromJson(res.toString());
state = state.copyWith(deviceInfo: deviceInfo); state = state.copyWith(deviceInfo: deviceInfo);
@@ -129,7 +137,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
firstName: '', firstName: '',
lastName: '', lastName: '',
profileImagePath: '', profileImagePath: '',
isFirstLogin: false, shouldChangePassword: false,
isAuthenticated: false, isAuthenticated: false,
isAdmin: false, isAdmin: false,
deviceInfo: DeviceInfoRemote( deviceInfo: DeviceInfoRemote(
@@ -151,15 +159,39 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
var deviceId = deviceInfo["deviceId"]; var deviceId = deviceInfo["deviceId"];
var deviceType = deviceInfo["deviceType"]; 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); state = state.copyWith(deviceInfo: deviceInfoRemote);
} }
updateUserProfileImagePath(String path) { updateUserProfileImagePath(String path) {
state = state.copyWith(profileImagePath: 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) { final authenticationProvider =
return AuthenticationNotifier(ref); 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:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/asset.provider.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/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
@@ -15,13 +16,17 @@ class LoginForm extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final usernameController = useTextEditingController.fromValue(TextEditingValue.empty); final usernameController =
final passwordController = useTextEditingController.fromValue(TextEditingValue.empty); useTextEditingController.fromValue(TextEditingValue.empty);
final serverEndpointController = useTextEditingController(text: 'http://your-server-ip:2283'); final passwordController =
useTextEditingController.fromValue(TextEditingValue.empty);
final serverEndpointController =
useTextEditingController(text: 'http://your-server-ip:2283/api');
final isSaveLoginInfo = useState<bool>(false); final isSaveLoginInfo = useState<bool>(false);
useEffect(() { useEffect(() {
var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey); var loginInfo =
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
if (loginInfo != null) { if (loginInfo != null) {
usernameController.text = loginInfo.email; usernameController.text = loginInfo.email;
@@ -64,11 +69,15 @@ class LoginForm extends HookConsumerWidget {
contentPadding: const EdgeInsets.symmetric(horizontal: 8), contentPadding: const EdgeInsets.symmetric(horizontal: 8),
dense: true, dense: true,
side: const BorderSide(color: Colors.grey, width: 1.5), side: const BorderSide(color: Colors.grey, width: 1.5),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
enableFeedback: true, enableFeedback: true,
title: const Text( title: const Text(
"Stay logged in", "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, value: isSaveLoginInfo.value,
onChanged: (switchValue) { onChanged: (switchValue) {
@@ -94,12 +103,16 @@ class LoginForm extends HookConsumerWidget {
class ServerEndpointInput extends StatelessWidget { class ServerEndpointInput extends StatelessWidget {
final TextEditingController controller; 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) { 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; return null;
} else {
return 'Please specify http:// or https://';
}
} }
@override @override
@@ -107,7 +120,10 @@ class ServerEndpointInput extends StatelessWidget {
return TextFormField( return TextFormField(
controller: controller, controller: controller,
decoration: const InputDecoration( 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, validator: _validateInput,
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
); );
@@ -131,8 +147,11 @@ class EmailInput extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextFormField( return TextFormField(
controller: controller, controller: controller,
decoration: decoration: const InputDecoration(
const InputDecoration(labelText: 'Email', border: OutlineInputBorder(), hintText: 'youremail@email.com'), labelText: 'Email',
border: OutlineInputBorder(),
hintText: 'youremail@email.com',
),
validator: _validateInput, validator: _validateInput,
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
); );
@@ -149,7 +168,10 @@ class PasswordInput extends StatelessWidget {
return TextFormField( return TextFormField(
obscureText: true, obscureText: true,
controller: controller, 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(); ref.watch(assetProvider.notifier).clearAllAsset();
var isAuthenticated = await ref var isAuthenticated = await ref
.read(authenticationProvider.notifier) .watch(authenticationProvider.notifier)
.login(emailController.text, passwordController.text, serverEndpointController.text, isSavedLoginInfo); .login(emailController.text, passwordController.text,
serverEndpointController.text, isSavedLoginInfo);
if (isAuthenticated) { if (isAuthenticated) {
// Resume backup (if enable) then navigate // Resume backup (if enable) then navigate
ref.watch(backupProvider.notifier).resumeBackup();
AutoRouter.of(context).pushNamed("/tab-controller-page"); 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 { } else {
ImmichToast.show( ImmichToast.show(
context: context, 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, 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()); 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 @override
String toString() { String toString() {
@@ -74,6 +75,10 @@ class CuratedLocation {
@override @override
int get hashCode { 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()); 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 @override
String toString() { String toString() {
@@ -75,6 +76,10 @@ class CuratedObject {
@override @override
int get hashCode { 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, searchTerm: searchTerm ?? this.searchTerm,
isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled, isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled,
searchSuggestion: searchSuggestion ?? this.searchSuggestion, searchSuggestion: searchSuggestion ?? this.searchSuggestion,
userSuggestedSearchTerms: userSuggestedSearchTerms ?? this.userSuggestedSearchTerms, userSuggestedSearchTerms:
userSuggestedSearchTerms ?? this.userSuggestedSearchTerms,
); );
} }
@@ -43,13 +44,15 @@ class SearchPageState {
searchTerm: map['searchTerm'] ?? '', searchTerm: map['searchTerm'] ?? '',
isSearchEnabled: map['isSearchEnabled'] ?? false, isSearchEnabled: map['isSearchEnabled'] ?? false,
searchSuggestion: List<String>.from(map['searchSuggestion']), searchSuggestion: List<String>.from(map['searchSuggestion']),
userSuggestedSearchTerms: List<String>.from(map['userSuggestedSearchTerms']), userSuggestedSearchTerms:
List<String>.from(map['userSuggestedSearchTerms']),
); );
} }
String toJson() => json.encode(toMap()); 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 @override
String toString() { String toString() {

View File

@@ -44,13 +44,15 @@ class SearchResultPageState {
isLoading: map['isLoading'] ?? false, isLoading: map['isLoading'] ?? false,
isSuccess: map['isSuccess'] ?? false, isSuccess: map['isSuccess'] ?? false,
isError: map['isError'] ?? 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()); 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 @override
String toString() { String toString() {
@@ -71,6 +73,9 @@ class SearchResultPageState {
@override @override
int get hashCode { 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'; import 'package:immich_mobile/modules/search/services/search.service.dart';
class SearchPageStateNotifier extends StateNotifier<SearchPageState> { class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
SearchPageStateNotifier() SearchPageStateNotifier(this._searchService)
: super( : super(
SearchPageState( SearchPageState(
searchTerm: "", searchTerm: "",
@@ -16,7 +16,7 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
), ),
); );
final SearchService _searchService = SearchService(); final SearchService _searchService;
void enableSearch() { void enableSearch() {
state = state.copyWith(isSearchEnabled: true); state = state.copyWith(isSearchEnabled: true);
@@ -54,29 +54,22 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
final searchPageStateProvider = final searchPageStateProvider =
StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) { StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
return SearchPageStateNotifier(); return SearchPageStateNotifier(ref.watch(searchServiceProvider));
}); });
final getCuratedLocationProvider = final getCuratedLocationProvider =
FutureProvider.autoDispose<List<CuratedLocation>>((ref) async { FutureProvider.autoDispose<List<CuratedLocation>>((ref) async {
final SearchService searchService = SearchService(); final SearchService searchService = ref.watch(searchServiceProvider);
var curatedLocation = await searchService.getCuratedLocation(); var curatedLocation = await searchService.getCuratedLocation();
if (curatedLocation != null) { return curatedLocation ?? [];
return curatedLocation;
} else {
return [];
}
}); });
final getCuratedObjectProvider = final getCuratedObjectProvider =
FutureProvider.autoDispose<List<CuratedObject>>((ref) async { FutureProvider.autoDispose<List<CuratedObject>>((ref) async {
final SearchService searchService = SearchService(); final SearchService searchService = ref.watch(searchServiceProvider);
var curatedObject = await searchService.getCuratedObjects(); var curatedObject = await searchService.getCuratedObjects();
if (curatedObject != null) {
return curatedObject; return curatedObject ?? [];
} else {
return [];
}
}); });

View File

@@ -7,31 +7,48 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> { class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
SearchResultPageNotifier() SearchResultPageNotifier(this._searchService)
: super(SearchResultPageState(searchResult: [], isError: false, isLoading: true, isSuccess: false)); : super(
SearchResultPageState(
searchResult: [],
isError: false,
isLoading: true,
isSuccess: false,
),
);
final SearchService _searchService = SearchService(); final SearchService _searchService;
void search(String searchTerm) async { 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); List<ImmichAsset>? assets = await _searchService.searchAsset(searchTerm);
if (assets != null) { 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 { } 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) { final searchResultPageProvider =
return SearchResultPageNotifier(); StateNotifierProvider<SearchResultPageNotifier, SearchResultPageState>(
(ref) {
return SearchResultPageNotifier(ref.watch(searchServiceProvider));
}); });
final searchResultGroupByDateTimeProvider = StateProvider((ref) { final searchResultGroupByDateTimeProvider = StateProvider((ref) {
var assets = ref.watch(searchResultPageProvider).searchResult; var assets = ref.watch(searchResultPageProvider).searchResult;
assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a)); assets.sortByCompare<DateTime>(
return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt))); (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 'dart:convert';
import 'package:flutter/material.dart'; 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_location.model.dart';
import 'package:immich_mobile/modules/search/models/curated_object.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/models/immich_asset.model.dart';
import 'package:immich_mobile/shared/services/network.service.dart'; import 'package:immich_mobile/shared/services/network.service.dart';
final searchServiceProvider =
Provider((ref) => SearchService(ref.watch(networkServiceProvider)));
class SearchService { class SearchService {
final NetworkService _networkService = NetworkService(); final NetworkService _networkService;
SearchService(this._networkService);
Future<List<String>?> getUserSuggestedSearchTerms() async { Future<List<String>?> getUserSuggestedSearchTerms() async {
try { try {
@@ -30,7 +35,8 @@ class SearchService {
List<dynamic> decodedData = jsonDecode(res.toString()); 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; return result;
} catch (e) { } catch (e) {
@@ -45,7 +51,8 @@ class SearchService {
List<dynamic> decodedData = jsonDecode(res.toString()); 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; return result;
} catch (e) { } catch (e) {
@@ -60,7 +67,8 @@ class SearchService {
List<dynamic> decodedData = jsonDecode(res.toString()); 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; return result;
} catch (e) { } 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'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
class SearchBar extends HookConsumerWidget with PreferredSizeWidget { 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 FocusNode searchFocusNode;
final Function(String) onSubmitted; 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'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
class SearchSuggestionList extends ConsumerWidget { 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; final Function(String) onSubmitted;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final searchTerm = ref.watch(searchPageStateProvider).searchTerm; final searchTerm = ref.watch(searchPageStateProvider).searchTerm;
final searchSuggestion = ref.watch(searchPageStateProvider).searchSuggestion; final searchSuggestion =
ref.watch(searchPageStateProvider).searchSuggestion;
return Container( 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( child: CustomScrollView(
slivers: [ slivers: [
SliverFillRemaining( SliverFillRemaining(

View File

@@ -5,7 +5,11 @@ import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/utils/capitalize_first_letter.dart'; import 'package:immich_mobile/utils/capitalize_first_letter.dart';
class ThumbnailWithInfo extends StatelessWidget { 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); : super(key: key);
final String textInfo; final String textInfo;
@@ -39,7 +43,9 @@ class ThumbnailWithInfo extends StatelessWidget {
height: 250, height: 250,
fit: BoxFit.cover, fit: BoxFit.cover,
imageUrl: imageUrl, 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 { class SearchPage extends HookConsumerWidget {
SearchPage({Key? key}) : super(key: key); SearchPage({Key? key}) : super(key: key);
late FocusNode searchFocusNode; FocusNode searchFocusNode = FocusNode();
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -176,9 +176,8 @@ class SearchPage extends HookConsumerWidget {
_buildThings() _buildThings()
], ],
), ),
isSearchEnabled if (isSearchEnabled)
? SearchSuggestionList(onSubmitted: _onSearchSubmitted) SearchSuggestionList(onSubmitted: _onSearchSubmitted),
: Container(),
], ],
), ),
), ),

View File

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

View File

@@ -36,16 +36,20 @@ class AlbumViewerPageState {
String toJson() => json.encode(toMap()); 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 @override
String toString() => 'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)'; String toString() =>
'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)';
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; 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 @override

View File

@@ -22,7 +22,8 @@ class AssetSelectionPageResult {
}) { }) {
return AssetSelectionPageResult( return AssetSelectionPageResult(
selectedNewAsset: selectedNewAsset ?? this.selectedNewAsset, selectedNewAsset: selectedNewAsset ?? this.selectedNewAsset,
selectedAdditionalAsset: selectedAdditionalAsset ?? this.selectedAdditionalAsset, selectedAdditionalAsset:
selectedAdditionalAsset ?? this.selectedAdditionalAsset,
isAlbumExist: isAlbumExist ?? this.isAlbumExist, isAlbumExist: isAlbumExist ?? this.isAlbumExist,
); );
} }
@@ -30,8 +31,12 @@ class AssetSelectionPageResult {
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
final result = <String, dynamic>{}; final result = <String, dynamic>{};
result.addAll({'selectedNewAsset': selectedNewAsset.map((x) => x.toMap()).toList()}); result.addAll(
result.addAll({'selectedAdditionalAsset': selectedAdditionalAsset.map((x) => x.toMap()).toList()}); {'selectedNewAsset': selectedNewAsset.map((x) => x.toMap()).toList()});
result.addAll({
'selectedAdditionalAsset':
selectedAdditionalAsset.map((x) => x.toMap()).toList()
});
result.addAll({'isAlbumExist': isAlbumExist}); result.addAll({'isAlbumExist': isAlbumExist});
return result; return result;
@@ -39,16 +44,18 @@ class AssetSelectionPageResult {
factory AssetSelectionPageResult.fromMap(Map<String, dynamic> map) { factory AssetSelectionPageResult.fromMap(Map<String, dynamic> map) {
return AssetSelectionPageResult( return AssetSelectionPageResult(
selectedNewAsset: Set<ImmichAsset>.from(map['selectedNewAsset']?.map((x) => ImmichAsset.fromMap(x))), selectedNewAsset: Set<ImmichAsset>.from(
selectedAdditionalAsset: map['selectedNewAsset']?.map((x) => ImmichAsset.fromMap(x))),
Set<ImmichAsset>.from(map['selectedAdditionalAsset']?.map((x) => ImmichAsset.fromMap(x))), selectedAdditionalAsset: Set<ImmichAsset>.from(
map['selectedAdditionalAsset']?.map((x) => ImmichAsset.fromMap(x))),
isAlbumExist: map['isAlbumExist'] ?? false, isAlbumExist: map['isAlbumExist'] ?? false,
); );
} }
String toJson() => json.encode(toMap()); 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 @override
String toString() => String toString() =>
@@ -66,5 +73,8 @@ class AssetSelectionPageResult {
} }
@override @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( return AssetSelectionState(
selectedMonths: selectedMonths ?? this.selectedMonths, selectedMonths: selectedMonths ?? this.selectedMonths,
selectedNewAssetsForAlbum: selectedNewAssetsForAlbum ?? this.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum:
selectedAdditionalAssetsForAlbum: selectedAdditionalAssetsForAlbum ?? this.selectedAdditionalAssetsForAlbum, selectedNewAssetsForAlbum ?? this.selectedNewAssetsForAlbum,
selectedAssetsInAlbumViewer: selectedAssetsInAlbumViewer ?? this.selectedAssetsInAlbumViewer, selectedAdditionalAssetsForAlbum: selectedAdditionalAssetsForAlbum ??
this.selectedAdditionalAssetsForAlbum,
selectedAssetsInAlbumViewer:
selectedAssetsInAlbumViewer ?? this.selectedAssetsInAlbumViewer,
isMultiselectEnable: isMultiselectEnable ?? this.isMultiselectEnable, isMultiselectEnable: isMultiselectEnable ?? this.isMultiselectEnable,
isAlbumExist: isAlbumExist ?? this.isAlbumExist, isAlbumExist: isAlbumExist ?? this.isAlbumExist,
); );
@@ -44,10 +47,18 @@ class AssetSelectionState {
final result = <String, dynamic>{}; final result = <String, dynamic>{};
result.addAll({'selectedMonths': selectedMonths.toList()}); result.addAll({'selectedMonths': selectedMonths.toList()});
result.addAll({'selectedNewAssetsForAlbum': selectedNewAssetsForAlbum.map((x) => x.toMap()).toList()}); result.addAll({
result 'selectedNewAssetsForAlbum':
.addAll({'selectedAdditionalAssetsForAlbum': selectedAdditionalAssetsForAlbum.map((x) => x.toMap()).toList()}); selectedNewAssetsForAlbum.map((x) => x.toMap()).toList()
result.addAll({'selectedAssetsInAlbumViewer': selectedAssetsInAlbumViewer.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({'isMultiselectEnable': isMultiselectEnable});
result.addAll({'isAlbumExist': isAlbumExist}); result.addAll({'isAlbumExist': isAlbumExist});
@@ -57,12 +68,14 @@ class AssetSelectionState {
factory AssetSelectionState.fromMap(Map<String, dynamic> map) { factory AssetSelectionState.fromMap(Map<String, dynamic> map) {
return AssetSelectionState( return AssetSelectionState(
selectedMonths: Set<String>.from(map['selectedMonths']), selectedMonths: Set<String>.from(map['selectedMonths']),
selectedNewAssetsForAlbum: selectedNewAssetsForAlbum: Set<ImmichAsset>.from(
Set<ImmichAsset>.from(map['selectedNewAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))), map['selectedNewAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))),
selectedAdditionalAssetsForAlbum: selectedAdditionalAssetsForAlbum: Set<ImmichAsset>.from(
Set<ImmichAsset>.from(map['selectedAdditionalAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))), map['selectedAdditionalAssetsForAlbum']
selectedAssetsInAlbumViewer: ?.map((x) => ImmichAsset.fromMap(x))),
Set<ImmichAsset>.from(map['selectedAssetsInAlbumViewer']?.map((x) => ImmichAsset.fromMap(x))), selectedAssetsInAlbumViewer: Set<ImmichAsset>.from(
map['selectedAssetsInAlbumViewer']
?.map((x) => ImmichAsset.fromMap(x))),
isMultiselectEnable: map['isMultiselectEnable'] ?? false, isMultiselectEnable: map['isMultiselectEnable'] ?? false,
isAlbumExist: map['isAlbumExist'] ?? false, isAlbumExist: map['isAlbumExist'] ?? false,
); );
@@ -70,7 +83,8 @@ class AssetSelectionState {
String toJson() => json.encode(toMap()); 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 @override
String toString() { String toString() {
@@ -85,8 +99,10 @@ class AssetSelectionState {
return other is AssetSelectionState && return other is AssetSelectionState &&
setEquals(other.selectedMonths, selectedMonths) && setEquals(other.selectedMonths, selectedMonths) &&
setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) && setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) &&
setEquals(other.selectedAdditionalAssetsForAlbum, selectedAdditionalAssetsForAlbum) && setEquals(other.selectedAdditionalAssetsForAlbum,
setEquals(other.selectedAssetsInAlbumViewer, selectedAssetsInAlbumViewer) && selectedAdditionalAssetsForAlbum) &&
setEquals(
other.selectedAssetsInAlbumViewer, selectedAssetsInAlbumViewer) &&
other.isMultiselectEnable == isMultiselectEnable && other.isMultiselectEnable == isMultiselectEnable &&
other.isAlbumExist == isAlbumExist; other.isAlbumExist == isAlbumExist;
} }

View File

@@ -38,7 +38,8 @@ class SharedAlbum {
ownerId: ownerId ?? this.ownerId, ownerId: ownerId ?? this.ownerId,
albumName: albumName ?? this.albumName, albumName: albumName ?? this.albumName,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
albumThumbnailAssetId: albumThumbnailAssetId ?? this.albumThumbnailAssetId, albumThumbnailAssetId:
albumThumbnailAssetId ?? this.albumThumbnailAssetId,
sharedUsers: sharedUsers ?? this.sharedUsers, sharedUsers: sharedUsers ?? this.sharedUsers,
assets: assets ?? this.assets, assets: assets ?? this.assets,
); );
@@ -69,16 +70,16 @@ class SharedAlbum {
albumName: map['albumName'] ?? '', albumName: map['albumName'] ?? '',
createdAt: map['createdAt'] ?? '', createdAt: map['createdAt'] ?? '',
albumThumbnailAssetId: map['albumThumbnailAssetId'], albumThumbnailAssetId: map['albumThumbnailAssetId'],
sharedUsers: List<User>.from(map['sharedUsers']?.map((x) => User.fromMap(x))), sharedUsers:
assets: map['assets'] != null List<User>.from(map['sharedUsers']?.map((x) => User.fromMap(x))),
? List<ImmichAsset>.from(map['assets']?.map((x) => ImmichAsset.fromMap(x))) assets: map['assets']?.map((x) => ImmichAsset.fromMap(x)).toList(),
: null,
); );
} }
String toJson() => json.encode(toMap()); 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 @override
String toString() { 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'; import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> { class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
AlbumViewerNotifier(this.ref) : super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false)); AlbumViewerNotifier(this.ref)
: super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
final Ref ref; final Ref ref;
@@ -28,10 +29,12 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
state = state.copyWith(editTitleText: "", isEditAlbum: false); state = state.copyWith(editTitleText: "", isEditAlbum: false);
} }
Future<bool> changeAlbumTitle(String albumId, String ownerId, String newAlbumTitle) async { Future<bool> changeAlbumTitle(
SharedAlbumService service = SharedAlbumService(); 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) { if (isSuccess) {
state = state.copyWith(editTitleText: "", isEditAlbum: false); 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); return AlbumViewerNotifier(ref);
}); });

View File

@@ -18,35 +18,48 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
state = state.copyWith(isAlbumExist: isAlbumExist); 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<ImmichAsset> currentAssetList = state.selectedNewAssetsForAlbum;
Set<String> currentMonthList = state.selectedMonths; Set<String> currentMonthList = state.selectedMonths;
currentMonthList.removeWhere((selectedMonth) => selectedMonth == removedMonth); currentMonthList
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
for (ImmichAsset asset in assetsInMonth) { for (ImmichAsset asset in assetsInMonth) {
currentAssetList.removeWhere((e) => e.id == asset.id); 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) { void addAdditionalAssets(List<ImmichAsset> assets) {
state = state.copyWith( state = state.copyWith(
selectedAdditionalAssetsForAlbum: {...state.selectedAdditionalAssetsForAlbum, ...assets}, selectedAdditionalAssetsForAlbum: {
...state.selectedAdditionalAssetsForAlbum,
...assets
},
); );
} }
void addAllAssetsInMonth(String month, List<ImmichAsset> assetsInMonth) { void addAllAssetsInMonth(String month, List<ImmichAsset> assetsInMonth) {
state = state.copyWith( state = state.copyWith(
selectedMonths: {...state.selectedMonths, month}, selectedMonths: {...state.selectedMonths, month},
selectedNewAssetsForAlbum: {...state.selectedNewAssetsForAlbum, ...assetsInMonth}, selectedNewAssetsForAlbum: {
...state.selectedNewAssetsForAlbum,
...assetsInMonth
},
); );
} }
void addNewAssets(List<ImmichAsset> assets) { void addNewAssets(List<ImmichAsset> assets) {
state = state.copyWith( 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) { void addAssetsInAlbumViewer(List<ImmichAsset> assets) {
state = state.copyWith( 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(); 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'; import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> { class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
SharedAlbumNotifier() : super([]); SharedAlbumNotifier(this._sharedAlbumService) : super([]);
final SharedAlbumService _sharedAlbumService = SharedAlbumService(); final SharedAlbumService _sharedAlbumService;
getAllSharedAlbums() async { getAllSharedAlbums() async {
List<SharedAlbum> sharedAlbums = List<SharedAlbum> sharedAlbums =
@@ -50,12 +50,13 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
final sharedAlbumProvider = final sharedAlbumProvider =
StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) { StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) {
return SharedAlbumNotifier(); return SharedAlbumNotifier(ref.watch(sharedAlbumServiceProvider));
}); });
final sharedAlbumDetailProvider = FutureProvider.autoDispose final sharedAlbumDetailProvider = FutureProvider.autoDispose
.family<SharedAlbum, String>((ref, albumId) async { .family<SharedAlbum, String>((ref, albumId) async {
final SharedAlbumService sharedAlbumService = SharedAlbumService(); final SharedAlbumService sharedAlbumService =
ref.watch(sharedAlbumServiceProvider);
return await sharedAlbumService.getAlbumDetail(albumId); 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/models/user.model.dart';
import 'package:immich_mobile/shared/services/user.service.dart'; import 'package:immich_mobile/shared/services/user.service.dart';
final suggestedSharedUsersProvider = FutureProvider.autoDispose<List<User>>((ref) async { final suggestedSharedUsersProvider =
UserService userService = UserService(); FutureProvider.autoDispose<List<User>>((ref) async {
UserService userService = ref.watch(userServiceProvider);
return await userService.getAllUsersInfo(); return await userService.getAllUsersInfo();
}); });

View File

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

View File

@@ -5,7 +5,11 @@ class AlbumActionOutlinedButton extends StatelessWidget {
final String labelText; final String labelText;
final IconData iconData; 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); : super(key: key);
@override @override
@@ -26,7 +30,8 @@ class AlbumActionOutlinedButton extends StatelessWidget {
icon: Icon(iconData, size: 15), icon: Icon(iconData, size: 15),
label: Text( label: Text(
labelText, 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, onPressed: onPressed,
), ),

View File

@@ -29,7 +29,8 @@ class AlbumTitleTextField extends ConsumerWidget {
ref.watch(albumTitleProvider.notifier).setAlbumTitle(v); ref.watch(albumTitleProvider.notifier).setAlbumTitle(v);
}, },
focusNode: albumTitleTextFieldFocusNode, 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, controller: albumTitleController,
onTap: () { onTap: () {
isAlbumTitleTextFieldFocus.value = true; isAlbumTitleTextFieldFocus.value = true;

View File

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

View File

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

View File

@@ -22,8 +22,10 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
var thumbnailRequestUrl = var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true'; '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
var deviceId = ref.watch(authenticationProvider).deviceId; var deviceId = ref.watch(authenticationProvider).deviceId;
final selectedAssetsInAlbumViewer = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer; final selectedAssetsInAlbumViewer =
final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable; ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
final isMultiSelectionEnable =
ref.watch(assetSelectionProvider).isMultiselectEnable;
_viewAsset() { _viewAsset() {
if (asset.type == 'IMAGE') { if (asset.type == 'IMAGE') {
@@ -39,7 +41,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
} else { } else {
AutoRouter.of(context).push( AutoRouter.of(context).push(
VideoViewerRoute( 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), asset: asset),
); );
} }
@@ -58,7 +61,9 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
_enableMultiSelection() { _enableMultiSelection() {
ref.watch(assetSelectionProvider.notifier).enableMultiselection(); ref.watch(assetSelectionProvider.notifier).enableMultiselection();
ref.watch(assetSelectionProvider.notifier).addAssetsInAlbumViewer([asset]); ref
.watch(assetSelectionProvider.notifier)
.addAssetsInAlbumViewer([asset]);
} }
_disableMultiSelection() { _disableMultiSelection() {
@@ -66,29 +71,25 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
} }
_buildVideoLabel() { _buildVideoLabel() {
if (asset.type == 'IMAGE') { return Positioned(
return Container(); top: 5,
} else { right: 5,
return Positioned( child: Row(
top: 5, children: [
right: 5, Text(
child: Row( asset.duration.toString().substring(0, 7),
children: [ style: const TextStyle(
Text(
asset.duration.toString().substring(0, 7),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white, color: Colors.white,
fontSize: 10,
), ),
], ),
), const Icon(
); Icons.play_circle_outline_rounded,
} color: Colors.white,
),
],
),
);
} }
_buildAssetStoreLocationIcon() { _buildAssetStoreLocationIcon() {
@@ -96,7 +97,9 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
right: 10, right: 10,
bottom: 5, bottom: 5,
child: Icon( 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, color: Colors.white,
size: 18, size: 18,
), ),
@@ -105,23 +108,20 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
_buildAssetSelectionIcon() { _buildAssetSelectionIcon() {
bool isSelected = selectedAssetsInAlbumViewer.contains(asset); bool isSelected = selectedAssetsInAlbumViewer.contains(asset);
if (isMultiSelectionEnable) {
return Positioned( return Positioned(
left: 10, left: 10,
top: 5, top: 5,
child: isSelected child: isSelected
? Icon( ? Icon(
Icons.check_circle_rounded, Icons.check_circle_rounded,
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
) )
: const Icon( : const Icon(
Icons.check_circle_outline_rounded, Icons.check_circle_outline_rounded,
color: Colors.white, color: Colors.white,
), ),
); );
} else {
return Container();
}
} }
_buildThumbnailImage() { _buildThumbnailImage() {
@@ -136,7 +136,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
imageUrl: thumbnailRequestUrl, imageUrl: thumbnailRequestUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250), fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale( progressIndicatorBuilder: (context, url, downloadProgress) =>
Transform.scale(
scale: 0.2, scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress), child: CircularProgressIndicator(value: downloadProgress.progress),
), ),
@@ -152,13 +153,17 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
_handleSelectionGesture() { _handleSelectionGesture() {
if (selectedAssetsInAlbumViewer.contains(asset)) { if (selectedAssetsInAlbumViewer.contains(asset)) {
ref.watch(assetSelectionProvider.notifier).removeAssetsInAlbumViewer([asset]); ref
.watch(assetSelectionProvider.notifier)
.removeAssetsInAlbumViewer([asset]);
if (selectedAssetsInAlbumViewer.isEmpty) { if (selectedAssetsInAlbumViewer.isEmpty) {
_disableMultiSelection(); _disableMultiSelection();
} }
} else { } else {
ref.watch(assetSelectionProvider.notifier).addAssetsInAlbumViewer([asset]); ref
.watch(assetSelectionProvider.notifier)
.addAssetsInAlbumViewer([asset]);
} }
} }
@@ -171,8 +176,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
children: [ children: [
_buildThumbnailImage(), _buildThumbnailImage(),
_buildAssetStoreLocationIcon(), _buildAssetStoreLocationIcon(),
_buildVideoLabel(), if (asset.type != 'IMAGE') _buildVideoLabel(),
_buildAssetSelectionIcon(), if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
], ],
), ),
), ),

View File

@@ -5,7 +5,8 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class AssetGridByMonth extends HookConsumerWidget { class AssetGridByMonth extends HookConsumerWidget {
final List<ImmichAsset> assetGroup; 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return SliverGrid( return SliverGrid(

View File

@@ -8,12 +8,15 @@ class MonthGroupTitle extends HookConsumerWidget {
final String month; final String month;
final List<ImmichAsset> assetGroup; 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final selectedDateGroup = ref.watch(assetSelectionProvider).selectedMonths; final selectedDateGroup = ref.watch(assetSelectionProvider).selectedMonths;
final selectedAssets = ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum; final selectedAssets =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist; final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
_handleTitleIconClick() { _handleTitleIconClick() {
@@ -21,10 +24,16 @@ class MonthGroupTitle extends HookConsumerWidget {
if (isAlbumExist) { if (isAlbumExist) {
if (selectedDateGroup.contains(month)) { if (selectedDateGroup.contains(month)) {
ref.watch(assetSelectionProvider.notifier).removeAssetsInMonth(month, []); ref
ref.watch(assetSelectionProvider.notifier).removeSelectedAdditionalAssets(assetGroup); .watch(assetSelectionProvider.notifier)
.removeAssetsInMonth(month, []);
ref
.watch(assetSelectionProvider.notifier)
.removeSelectedAdditionalAssets(assetGroup);
} else { } else {
ref.watch(assetSelectionProvider.notifier).addAllAssetsInMonth(month, []); ref
.watch(assetSelectionProvider.notifier)
.addAllAssetsInMonth(month, []);
// Deep clone assetGroup // Deep clone assetGroup
var assetGroupWithNewItems = [...assetGroup]; var assetGroupWithNewItems = [...assetGroup];
@@ -33,13 +42,19 @@ class MonthGroupTitle extends HookConsumerWidget {
assetGroupWithNewItems.removeWhere((a) => a.id == selectedAsset.id); assetGroupWithNewItems.removeWhere((a) => a.id == selectedAsset.id);
} }
ref.watch(assetSelectionProvider.notifier).addAdditionalAssets(assetGroupWithNewItems); ref
.watch(assetSelectionProvider.notifier)
.addAdditionalAssets(assetGroupWithNewItems);
} }
} else { } else {
if (selectedDateGroup.contains(month)) { if (selectedDateGroup.contains(month)) {
ref.watch(assetSelectionProvider.notifier).removeAssetsInMonth(month, assetGroup); ref
.watch(assetSelectionProvider.notifier)
.removeAssetsInMonth(month, assetGroup);
} else { } 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( return SliverToBoxAdapter(
child: Padding( 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( child: Row(
children: [ children: [
GestureDetector( GestureDetector(

View File

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

View File

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

View File

@@ -14,7 +14,8 @@ class SharingSliverAppBar extends StatelessWidget {
floating: false, floating: false,
pinned: true, pinned: true,
snap: false, snap: false,
leading: Container(), automaticallyImplyLeading: false,
// leading: Container(),
// elevation: 0, // elevation: 0,
title: Text( title: Text(
'IMMICH', 'IMMICH',
@@ -37,11 +38,13 @@ class SharingSliverAppBar extends StatelessWidget {
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4.0),
child: TextButton.icon( child: TextButton.icon(
style: ButtonStyle( 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), // foregroundColor: MaterialStateProperty.all(Colors.white),
), ),
onPressed: () { onPressed: () {
AutoRouter.of(context).push(const CreateSharedAlbumRoute()); AutoRouter.of(context)
.push(const CreateSharedAlbumRoute());
}, },
icon: const Icon( icon: const Icon(
Icons.photo_album_outlined, Icons.photo_album_outlined,
@@ -49,7 +52,8 @@ class SharingSliverAppBar extends StatelessWidget {
), ),
label: const Text( label: const Text(
"Create shared album", "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), padding: const EdgeInsets.only(left: 4.0),
child: TextButton.icon( child: TextButton.icon(
style: ButtonStyle( 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), // foregroundColor: MaterialStateProperty.all(Colors.white),
), ),
onPressed: null, onPressed: null,
@@ -69,7 +74,8 @@ class SharingSliverAppBar extends StatelessWidget {
), ),
label: const Text( label: const Text(
"Share with partner", "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 /// 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. /// If they exist, add to selected asset state to show they are already selected.
void _onAddPhotosPressed(SharedAlbum albumInfo) async { void _onAddPhotosPressed(SharedAlbum albumInfo) async {
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) { if (albumInfo.assets?.isNotEmpty == true) {
ref ref
.watch(assetSelectionProvider.notifier) .watch(assetSelectionProvider.notifier)
.addNewAssets(albumInfo.assets!.toList()); .addNewAssets(albumInfo.assets!.toList());
@@ -53,8 +53,10 @@ class AlbumViewerPage extends HookConsumerWidget {
if (returnPayload.selectedAdditionalAsset.isNotEmpty) { if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
ImmichLoadingOverlayController.appLoader.show(); ImmichLoadingOverlayController.appLoader.show();
var isSuccess = await SharedAlbumService().addAdditionalAssetToAlbum( var isSuccess = await ref
returnPayload.selectedAdditionalAsset, albumId); .watch(sharedAlbumServiceProvider)
.addAdditionalAssetToAlbum(
returnPayload.selectedAdditionalAsset, albumId);
if (isSuccess) { if (isSuccess) {
ref.refresh(sharedAlbumDetailProvider(albumId)); ref.refresh(sharedAlbumDetailProvider(albumId));
@@ -77,7 +79,8 @@ class AlbumViewerPage extends HookConsumerWidget {
if (sharedUserIds != null) { if (sharedUserIds != null) {
ImmichLoadingOverlayController.appLoader.show(); ImmichLoadingOverlayController.appLoader.show();
var isSuccess = await SharedAlbumService() var isSuccess = await ref
.watch(sharedAlbumServiceProvider)
.addAdditionalUserToAlbum(sharedUserIds, albumId); .addAdditionalUserToAlbum(sharedUserIds, albumId);
if (isSuccess) { if (isSuccess) {
@@ -106,32 +109,28 @@ class AlbumViewerPage extends HookConsumerWidget {
} }
Widget _buildAlbumDateRange(SharedAlbum albumInfo) { Widget _buildAlbumDateRange(SharedAlbum albumInfo) {
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) { String startDate = "";
String startDate = ""; DateTime parsedStartDate =
DateTime parsedStartDate = DateTime.parse(albumInfo.assets!.first.createdAt);
DateTime.parse(albumInfo.assets!.first.createdAt); DateTime parsedEndDate = DateTime.parse(
DateTime parsedEndDate = albumInfo.assets?.last.createdAt ?? '11111111'); //Need default.
DateTime.parse(albumInfo.assets!.last.createdAt);
if (parsedStartDate.year == parsedEndDate.year) { if (parsedStartDate.year == parsedEndDate.year) {
startDate = DateFormat('LLL d').format(parsedStartDate); startDate = DateFormat('LLL d').format(parsedStartDate);
} else {
startDate = DateFormat('LLL d, y').format(parsedStartDate);
}
String endDate = DateFormat('LLL d, y').format(parsedEndDate);
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8),
child: Text(
"$startDate-$endDate",
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey),
),
);
} else { } else {
return Container(); startDate = DateFormat('LLL d, y').format(parsedStartDate);
} }
String endDate = DateFormat('LLL d, y').format(parsedEndDate);
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8),
child: Text(
"$startDate-$endDate",
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey),
),
);
} }
Widget _buildHeader(SharedAlbum albumInfo) { Widget _buildHeader(SharedAlbum albumInfo) {
@@ -140,7 +139,8 @@ class AlbumViewerPage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildTitle(albumInfo), _buildTitle(albumInfo),
_buildAlbumDateRange(albumInfo), if (albumInfo.assets?.isNotEmpty == true)
_buildAlbumDateRange(albumInfo),
SizedBox( SizedBox(
height: 60, height: 60,
child: ListView.builder( child: ListView.builder(
@@ -172,7 +172,7 @@ class AlbumViewerPage extends HookConsumerWidget {
} }
Widget _buildImageGrid(SharedAlbum albumInfo) { Widget _buildImageGrid(SharedAlbum albumInfo) {
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) { if (albumInfo.assets?.isNotEmpty == true) {
return SliverPadding( return SliverPadding(
padding: const EdgeInsets.only(top: 10.0), padding: const EdgeInsets.only(top: 10.0),
sliver: SliverGrid( sliver: SliverGrid(
@@ -206,13 +206,12 @@ class AlbumViewerPage extends HookConsumerWidget {
onPressed: () => _onAddPhotosPressed(albumInfo), onPressed: () => _onAddPhotosPressed(albumInfo),
labelText: "Add photos", labelText: "Add photos",
), ),
userId == albumInfo.ownerId if (userId == albumInfo.ownerId)
? AlbumActionOutlinedButton( AlbumActionOutlinedButton(
iconData: Icons.person_add_alt_rounded, iconData: Icons.person_add_alt_rounded,
onPressed: () => _onAddUsersPressed(albumInfo), onPressed: () => _onAddUsersPressed(albumInfo),
labelText: "Add users", labelText: "Add users",
) ),
: Container(),
], ],
), ),
), ),

View File

@@ -74,23 +74,22 @@ class AssetSelectionPage extends HookConsumerWidget {
), ),
centerTitle: false, centerTitle: false,
actions: [ actions: [
(!isAlbumExist && selectedAssets.isNotEmpty) || if ((!isAlbumExist && selectedAssets.isNotEmpty) ||
(isAlbumExist && newAssetsForAlbum.isNotEmpty) (isAlbumExist && newAssetsForAlbum.isNotEmpty))
? TextButton( TextButton(
onPressed: () { onPressed: () {
var payload = AssetSelectionPageResult( var payload = AssetSelectionPageResult(
isAlbumExist: isAlbumExist, isAlbumExist: isAlbumExist,
selectedAdditionalAsset: newAssetsForAlbum, selectedAdditionalAsset: newAssetsForAlbum,
selectedNewAsset: selectedAssets, selectedNewAsset: selectedAssets,
); );
AutoRouter.of(context).pop(payload); AutoRouter.of(context).pop(payload);
}, },
child: const Text( child: const Text(
"Add", "Add",
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
) ),
: Container()
], ],
), ),
body: _buildBody(), body: _buildBody(),

View File

@@ -113,26 +113,22 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
} }
_buildControlButton() { _buildControlButton() {
if (selectedAssets.isNotEmpty) { return Padding(
return Padding( padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16),
padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16), child: SizedBox(
child: SizedBox( height: 30,
height: 30, child: ListView(
child: ListView( scrollDirection: Axis.horizontal,
scrollDirection: Axis.horizontal, children: [
children: [ AlbumActionOutlinedButton(
AlbumActionOutlinedButton( iconData: Icons.add_photo_alternate_outlined,
iconData: Icons.add_photo_alternate_outlined, onPressed: _onSelectPhotosButtonPressed,
onPressed: _onSelectPhotosButtonPressed, labelText: "Add photos",
labelText: "Add photos", ),
), ],
],
),
), ),
); ),
} );
return Container();
} }
_buildSelectedImageGrid() { _buildSelectedImageGrid() {
@@ -196,7 +192,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
elevation: 5, elevation: 5,
leading: Container(), automaticallyImplyLeading: false,
// leading: Container(),
pinned: true, pinned: true,
floating: false, floating: false,
bottom: PreferredSize( bottom: PreferredSize(
@@ -204,7 +201,7 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
child: Column( child: Column(
children: [ children: [
_buildTitleInputField(), _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 { class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
final SharedAlbum albumInfo; final SharedAlbum albumInfo;
const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo}) : super(key: key); const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo})
: super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { 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>>({}); final sharedUsersList = useState<Set<User>>({});
_addNewUsersHandler() { _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) { _buildTileIcon(User user) {
@@ -32,7 +35,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
); );
} else { } else {
return CircleAvatar( 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), backgroundColor: Theme.of(context).primaryColor.withAlpha(50),
); );
} }
@@ -49,7 +53,10 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15), backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
label: Text( label: Text(
user.email, 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), padding: EdgeInsets.all(16.0),
child: Text( child: Text(
'Suggestions', 'Suggestions',
style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), style: TextStyle(
fontSize: 14,
color: Colors.grey,
fontWeight: FontWeight.bold),
), ),
), ),
ListView.builder( ListView.builder(
@@ -75,14 +85,20 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
leading: _buildTileIcon(users[index]), leading: _buildTileIcon(users[index]),
title: Text( title: Text(
users[index].email, users[index].email,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold),
), ),
onTap: () { onTap: () {
if (sharedUsersList.value.contains(users[index])) { if (sharedUsersList.value.contains(users[index])) {
sharedUsersList.value = sharedUsersList.value = sharedUsersList.value
sharedUsersList.value.where((selectedUser) => selectedUser.id != users[index].id).toSet(); .where((selectedUser) =>
selectedUser.id != users[index].id)
.toSet();
} else { } else {
sharedUsersList.value = {...sharedUsersList.value, users[index]}; sharedUsersList.value = {
...sharedUsersList.value,
users[index]
};
} }
}, },
); );
@@ -109,7 +125,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: sharedUsersList.value.isEmpty ? null : _addNewUsersHandler, onPressed:
sharedUsersList.value.isEmpty ? null : _addNewUsersHandler,
child: const Text( child: const Text(
"Add", "Add",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
@@ -120,7 +137,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
body: suggestedShareUsers.when( body: suggestedShareUsers.when(
data: (users) { data: (users) {
for (var sharedUsers in albumInfo.sharedUsers) { 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); return _buildUserList(users);

View File

@@ -16,24 +16,28 @@ class SelectUserForSharingPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final sharedUsersList = useState<Set<User>>({}); final sharedUsersList = useState<Set<User>>({});
AsyncValue<List<User>> suggestedShareUsers = ref.watch(suggestedSharedUsersProvider); AsyncValue<List<User>> suggestedShareUsers =
ref.watch(suggestedSharedUsersProvider);
_createSharedAlbum() async { _createSharedAlbum() async {
var isSuccess = await SharedAlbumService().createSharedAlbum( var isSuccess =
ref.watch(albumTitleProvider), await ref.watch(sharedAlbumServiceProvider).createSharedAlbum(
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum, ref.watch(albumTitleProvider),
sharedUsersList.value.map((userInfo) => userInfo.id).toList(), ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
); sharedUsersList.value.map((userInfo) => userInfo.id).toList(),
);
if (isSuccess) { if (isSuccess) {
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums(); await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
ref.watch(assetSelectionProvider.notifier).removeAll(); ref.watch(assetSelectionProvider.notifier).removeAll();
ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); 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) { _buildTileIcon(User user) {
@@ -47,7 +51,8 @@ class SelectUserForSharingPage extends HookConsumerWidget {
); );
} else { } else {
return CircleAvatar( 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), backgroundColor: Theme.of(context).primaryColor.withAlpha(50),
); );
} }
@@ -64,7 +69,10 @@ class SelectUserForSharingPage extends HookConsumerWidget {
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15), backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
label: Text( label: Text(
user.email, 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), padding: EdgeInsets.all(16.0),
child: Text( child: Text(
'Suggestions', 'Suggestions',
style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), style: TextStyle(
fontSize: 14,
color: Colors.grey,
fontWeight: FontWeight.bold),
), ),
), ),
ListView.builder( ListView.builder(
@@ -90,14 +101,20 @@ class SelectUserForSharingPage extends HookConsumerWidget {
leading: _buildTileIcon(users[index]), leading: _buildTileIcon(users[index]),
title: Text( title: Text(
users[index].email, users[index].email,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold),
), ),
onTap: () { onTap: () {
if (sharedUsersList.value.contains(users[index])) { if (sharedUsersList.value.contains(users[index])) {
sharedUsersList.value = sharedUsersList.value = sharedUsersList.value
sharedUsersList.value.where((selectedUser) => selectedUser.id != users[index].id).toSet(); .where((selectedUser) =>
selectedUser.id != users[index].id)
.toSet();
} else { } else {
sharedUsersList.value = {...sharedUsersList.value, users[index]}; sharedUsersList.value = {
...sharedUsersList.value,
users[index]
};
} }
}, },
); );
@@ -124,7 +141,8 @@ class SelectUserForSharingPage extends HookConsumerWidget {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: sharedUsersList.value.isEmpty ? null : _createSharedAlbum, onPressed:
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
child: const Text( child: const Text(
"Create Album", "Create Album",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),

View File

@@ -29,12 +29,14 @@ class SharingPage extends HookConsumerWidget {
return SliverList( return SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
String thumbnailUrl = sharedAlbums[index].albumThumbnailAssetId != null String thumbnailUrl = sharedAlbums[index].albumThumbnailAssetId !=
null
? "$thumbnailRequestUrl/${sharedAlbums[index].albumThumbnailAssetId}" ? "$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"; : "https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60";
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), contentPadding:
const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
leading: ClipRRect( leading: ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: FadeInImage( child: FadeInImage(
@@ -44,7 +46,9 @@ class SharingPage extends HookConsumerWidget {
placeholder: MemoryImage(kTransparentImage), placeholder: MemoryImage(kTransparentImage),
image: NetworkImage( image: NetworkImage(
thumbnailUrl, thumbnailUrl,
headers: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, headers: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
), ),
fadeInDuration: const Duration(milliseconds: 200), fadeInDuration: const Duration(milliseconds: 200),
fadeOutDuration: const Duration(milliseconds: 200), fadeOutDuration: const Duration(milliseconds: 200),
@@ -54,10 +58,14 @@ class SharingPage extends HookConsumerWidget {
sharedAlbums[index].albumName, sharedAlbums[index].albumName,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, 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: () { 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:flutter/material.dart';
import 'package:immich_mobile/modules/backup/views/album_preview_page.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/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/login/views/login_page.dart';
import 'package:immich_mobile/modules/home/views/home_page.dart'; import 'package:immich_mobile/modules/home/views/home_page.dart';
import 'package:immich_mobile/modules/search/views/search_page.dart'; import 'package:immich_mobile/modules/search/views/search_page.dart';
@@ -30,6 +31,7 @@ part 'router.gr.dart';
routes: <AutoRoute>[ routes: <AutoRoute>[
AutoRoute(page: SplashScreenPage, initial: true), AutoRoute(page: SplashScreenPage, initial: true),
AutoRoute(page: LoginPage), AutoRoute(page: LoginPage),
AutoRoute(page: ChangePasswordPage),
CustomRoute( CustomRoute(
page: TabControllerPage, page: TabControllerPage,
guards: [AuthGuard], guards: [AuthGuard],

View File

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

View File

@@ -149,7 +149,8 @@ class ImmichExif {
String toJson() => json.encode(toMap()); 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 @override
String toString() { String toString() {

View File

@@ -95,7 +95,8 @@ class ImmichAsset extends Equatable {
String toJson() => json.encode(toMap()); 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 @override
String toString() { String toString() {

View File

@@ -85,13 +85,15 @@ class ImmichAssetWithExif {
originalPath: map['originalPath'] ?? '', originalPath: map['originalPath'] ?? '',
isFavorite: map['isFavorite'] ?? false, isFavorite: map['isFavorite'] ?? false,
duration: map['duration'], 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()); 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 @override
String toString() { String toString() {

View File

@@ -34,16 +34,20 @@ class MapboxInfo {
String toJson() => json.encode(toMap()); 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 @override
String toString() => 'MapboxInfo(isEnable: $isEnable, mapboxSecret: $mapboxSecret)'; String toString() =>
'MapboxInfo(isEnable: $isEnable, mapboxSecret: $mapboxSecret)';
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; 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 @override

View File

@@ -64,7 +64,8 @@ class ServerInfo {
String toJson() => json.encode(toMap()); 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 @override
String toString() { String toString() {

View File

@@ -26,7 +26,8 @@ class ServerInfoState {
mapboxInfo: mapboxInfo ?? this.mapboxInfo, mapboxInfo: mapboxInfo ?? this.mapboxInfo,
serverVersion: serverVersion ?? this.serverVersion, serverVersion: serverVersion ?? this.serverVersion,
isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch, isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch,
versionMismatchErrorMessage: versionMismatchErrorMessage ?? this.versionMismatchErrorMessage, versionMismatchErrorMessage:
versionMismatchErrorMessage ?? this.versionMismatchErrorMessage,
); );
} }
@@ -50,7 +51,8 @@ class ServerInfoState {
String toJson() => json.encode(toMap()); 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 @override
String toString() { String toString() {

View File

@@ -47,7 +47,8 @@ class ServerVersion {
String toJson() => json.encode(toMap()); 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 @override
String toString() { String toString() {

View File

@@ -36,16 +36,20 @@ class UploadProfileImageResponse {
String toJson() => json.encode(toMap()); 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 @override
String toString() => 'UploadProfileImageReponse(userId: $userId, profileImagePath: $profileImagePath)'; String toString() =>
'UploadProfileImageReponse(userId: $userId, profileImagePath: $profileImagePath)';
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; 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 @override

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