Compare commits
5 Commits
v1.7.0_11-
...
v1.8.0_12-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c4243b3d0 | ||
|
|
38e0178c81 | ||
|
|
c5c7a134dd | ||
|
|
da9eb61532 | ||
|
|
c1ccf026f0 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,3 +1,4 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: alextran1502
|
github: alextran1502
|
||||||
|
custom: https://www.buymeacoffee.com/altran1502?new=1
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
[ ] Up version in [docker/docker-compose.gpu.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
|
[ ] Up version in [docker/docker-compose.gpu.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
|
||||||
|
|
||||||
[ ] Up version in [docker/docker-compose.dev.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
|
[ ] Up version in [docker/docker-compose.dev.yml](/docker/docker-compose.dev.yml) for `immich_server` service
|
||||||
|
|
||||||
[ ] Up version in [server/src/constants/server_version.constant.ts](/server/src/constants/server_version.constant.ts)
|
[ ] Up version in [server/src/constants/server_version.constant.ts](/server/src/constants/server_version.constant.ts)
|
||||||
|
|
||||||
|
|||||||
75
README.md
75
README.md
@@ -66,17 +66,25 @@ This project is under heavy development, there will be continous functions, feat
|
|||||||
- Show curated objects on the search page
|
- Show curated objects on the search page
|
||||||
- Shared album with users on the same server
|
- Shared album with users on the same server
|
||||||
|
|
||||||
# Development
|
# System Requirement
|
||||||
|
|
||||||
You can use docker compose for development, there are several services that compose Immich
|
**OS**: Preferred Linux-based operating system (Ubuntu, Debian, MacOS...etc). I haven't tested with `Docker for Windows` as well as `WSL` on Windows
|
||||||
|
|
||||||
1. NestJs
|
**RAM**: At least 2GB, preffered 4GB.
|
||||||
2. PostgreSQL
|
|
||||||
3. Redis
|
|
||||||
4. Nginx
|
|
||||||
5. TensorFlow
|
|
||||||
|
|
||||||
## Populate .env file
|
**Cores**: At least 2 cores, preffered 4 cores.
|
||||||
|
|
||||||
|
# Development and Testing out the application
|
||||||
|
|
||||||
|
You can use docker compose for development and testing out the application, there are several services that compose Immich:
|
||||||
|
|
||||||
|
1. **NestJs** - Backend of the application
|
||||||
|
2. **PostgreSQL** - Main database of the application
|
||||||
|
3. **Redis** - For sharing websocket instance between docker instances and background tasks message queue.
|
||||||
|
4. **Nginx** - Load balancing and optimized file uploading.
|
||||||
|
5. **TensorFlow** - Object Detection and Image Classification.
|
||||||
|
|
||||||
|
## Step 1: Populate .env file
|
||||||
|
|
||||||
Navigate to `docker` directory and run
|
Navigate to `docker` directory and run
|
||||||
|
|
||||||
@@ -90,15 +98,44 @@ Notice that if set `ENABLE_MAPBOX` to `true`, you will have to provide `MAPBOX_K
|
|||||||
|
|
||||||
Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is owned by the user that run the `docker-compose` command below.
|
Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is owned by the user that run the `docker-compose` command below.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database
|
||||||
|
DB_USERNAME=postgres
|
||||||
|
DB_PASSWORD=postgres
|
||||||
|
DB_DATABASE_NAME=immich
|
||||||
|
|
||||||
|
# Upload File Config
|
||||||
|
UPLOAD_LOCATION=<put-the-path-of-the-upload-folder-here>
|
||||||
|
|
||||||
|
# JWT SECRET
|
||||||
|
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
||||||
|
|
||||||
|
# MAPBOX
|
||||||
|
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
||||||
|
ENABLE_MAPBOX=false
|
||||||
|
MAPBOX_KEY=
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Start the server
|
||||||
|
|
||||||
To start, run
|
To start, run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V
|
docker-compose -f ./docker/docker-compose.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you have a few thousand photos/videos, I suggest running docker-compose with scaling option for the `immich_server` container to handle high I/O load when using fast scrolling.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f ./docker/docker-compose.yml up --scale immich_server=5
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
The server will be running at `http://your-ip:2283` through `Nginx`
|
The server will be running at `http://your-ip:2283` through `Nginx`
|
||||||
|
|
||||||
## Register User
|
## Step 3: Register User
|
||||||
|
|
||||||
Use the command below on your terminal to create user as we don't have user interface for this function yet.
|
Use the command below on your terminal to create user as we don't have user interface for this function yet.
|
||||||
|
|
||||||
@@ -111,10 +148,12 @@ curl --location --request POST 'http://your-server-ip:2283/auth/signUp' \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run mobile app
|
## Step 4: Run mobile app
|
||||||
|
|
||||||
|
The app is distributed on several platforms below.
|
||||||
|
|
||||||
## F-Droid
|
## F-Droid
|
||||||
You can get the app on F-droid by cliking the image below.
|
You can get the app on F-droid by clicking the image below.
|
||||||
|
|
||||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||||
alt="Get it on F-Droid"
|
alt="Get it on F-Droid"
|
||||||
@@ -123,11 +162,7 @@ You can get the app on F-droid by cliking the image below.
|
|||||||
|
|
||||||
## Android
|
## Android
|
||||||
|
|
||||||
#### Download latest `apk` in release tab and run on your phone. You can follow this guide on how to do that
|
#### Get the app on Google Play Store [here](https://play.google.com/store/apps/details?id=app.alextran.immich)
|
||||||
|
|
||||||
- [Run APK on Android](https://www.lifewire.com/install-apk-on-android-4177185)
|
|
||||||
|
|
||||||
#### You can also download the app from Google Play Store [here](https://play.google.com/store/apps/details?id=app.alextran.immich)
|
|
||||||
|
|
||||||
*The App version might be lagging behind the latest release due to the review process.*
|
*The App version might be lagging behind the latest release due to the review process.*
|
||||||
|
|
||||||
@@ -137,7 +172,7 @@ You can get the app on F-droid by cliking the image below.
|
|||||||
|
|
||||||
## iOS
|
## iOS
|
||||||
|
|
||||||
#### You can download the app from Apple AppStore [here](https://apps.apple.com/us/app/immich/id1613945652):
|
#### Get the app on Apple AppStore [here](https://apps.apple.com/us/app/immich/id1613945652):
|
||||||
|
|
||||||
*The App version might be lagging behind the latest release due to the review process.*
|
*The App version might be lagging behind the latest release due to the review process.*
|
||||||
|
|
||||||
@@ -148,7 +183,9 @@ You can get the app on F-droid by cliking the image below.
|
|||||||
|
|
||||||
# Support
|
# Support
|
||||||
|
|
||||||
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsore**](https://github.com/sponsors/alextran1502).
|
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsore**](https://github.com/sponsors/alextran1502), or one time donation with Buy Me a coffee link below.
|
||||||
|
|
||||||
|
[](https://www.buymeacoffee.com/altran1502)
|
||||||
|
|
||||||
This is also a meaningful way to give me motivation and encounragment to continue working on the app.
|
This is also a meaningful way to give me motivation and encounragment to continue working on the app.
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
# Database
|
# Database
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_PASSWORD=postgres
|
DB_PASSWORD=postgres
|
||||||
DB_DATABASE_NAME=
|
DB_DATABASE_NAME=immich
|
||||||
|
|
||||||
# Upload File Config
|
# Upload File Config
|
||||||
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
||||||
|
|
||||||
# JWT SECRET
|
# JWT SECRET
|
||||||
JWT_SECRET=
|
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
||||||
|
|
||||||
# MAPBOX
|
# MAPBOX
|
||||||
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
||||||
ENABLE_MAPBOX=
|
ENABLE_MAPBOX=false
|
||||||
MAPBOX_KEY=
|
MAPBOX_KEY=
|
||||||
@@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich_server:
|
||||||
image: immich-server-dev:1.7.0
|
image: immich-server-dev:1.8.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -24,7 +24,7 @@ services:
|
|||||||
- immich_network
|
- immich_network
|
||||||
|
|
||||||
immich_microservices:
|
immich_microservices:
|
||||||
image: immich-microservices-dev:1.7.0
|
image: immich-microservices-dev:1.8.0
|
||||||
build:
|
build:
|
||||||
context: ../microservices
|
context: ../microservices
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich_server:
|
||||||
image: immich-server-dev:1.7.0
|
image: immich-server-dev:1.8.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -22,7 +22,7 @@ services:
|
|||||||
- immich_network
|
- immich_network
|
||||||
|
|
||||||
immich_microservices:
|
immich_microservices:
|
||||||
image: immich-microservices-dev:1.7.0
|
image: immich-microservices-dev:1.8.0
|
||||||
build:
|
build:
|
||||||
context: ../microservices
|
context: ../microservices
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich_server:
|
||||||
image: immich-server:1.7.0
|
image: altran1502/immich-server:v1.8.0_12-dev
|
||||||
build:
|
|
||||||
context: ../server
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
@@ -23,10 +20,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
immich_microservices:
|
immich_microservices:
|
||||||
image: immich-microservices:1.7.0
|
image: altran1502/immich-microservices:v1.8.0_12-dev
|
||||||
build:
|
|
||||||
context: ../microservices
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
expose:
|
expose:
|
||||||
- "3001"
|
- "3001"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
* Album name is now editable
|
||||||
@@ -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.7.0"
|
version_number: "1.8.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ const String userInfoBox = "immichBoxUserInfo"; // Box
|
|||||||
const String accessTokenKey = "immichBoxAccessTokenKey"; // Key 1
|
const String accessTokenKey = "immichBoxAccessTokenKey"; // Key 1
|
||||||
const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2
|
const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2
|
||||||
|
|
||||||
// SERVER ENDPOINT
|
// Server endpoint
|
||||||
const String serverEndpointKey = 'immichBoxServerEndpoint';
|
const String serverEndpointKey = 'immichBoxServerEndpoint';
|
||||||
|
|
||||||
// KEY
|
// Login Info
|
||||||
const String hiveAllAsssetKey = "allAssets";
|
const String hiveLoginInfoBox = "immichLoginInfoBox";
|
||||||
const String hiveBackupProgressKey = "backupProgressAssets";
|
const String savedLoginInfoKey = "immichSavedLoginInfoKey";
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||||
@@ -15,7 +16,9 @@ import 'constants/hive_box.dart';
|
|||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
await Hive.initFlutter();
|
await Hive.initFlutter();
|
||||||
|
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
|
||||||
await Hive.openBox(userInfoBox);
|
await Hive.openBox(userInfoBox);
|
||||||
|
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
|
||||||
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(
|
const SystemUiOverlayStyle(
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:hive/hive.dart';
|
||||||
|
|
||||||
|
part 'hive_saved_login_info.model.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: 0)
|
||||||
|
class HiveSavedLoginInfo {
|
||||||
|
@HiveField(0)
|
||||||
|
String email;
|
||||||
|
|
||||||
|
@HiveField(1)
|
||||||
|
String password;
|
||||||
|
|
||||||
|
@HiveField(2)
|
||||||
|
String serverUrl;
|
||||||
|
|
||||||
|
@HiveField(3)
|
||||||
|
bool isSaveLogin;
|
||||||
|
|
||||||
|
HiveSavedLoginInfo({required this.email, required this.password, required this.serverUrl, required this.isSaveLogin});
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'hive_saved_login_info.model.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// TypeAdapterGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
class HiveSavedLoginInfoAdapter extends TypeAdapter<HiveSavedLoginInfo> {
|
||||||
|
@override
|
||||||
|
final int typeId = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
HiveSavedLoginInfo read(BinaryReader reader) {
|
||||||
|
final numOfFields = reader.readByte();
|
||||||
|
final fields = <int, dynamic>{
|
||||||
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
|
};
|
||||||
|
return HiveSavedLoginInfo(
|
||||||
|
email: fields[0] as String,
|
||||||
|
password: fields[1] as String,
|
||||||
|
serverUrl: fields[2] as String,
|
||||||
|
isSaveLogin: fields[3] as bool,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(BinaryWriter writer, HiveSavedLoginInfo obj) {
|
||||||
|
writer
|
||||||
|
..writeByte(4)
|
||||||
|
..writeByte(0)
|
||||||
|
..write(obj.email)
|
||||||
|
..writeByte(1)
|
||||||
|
..write(obj.password)
|
||||||
|
..writeByte(2)
|
||||||
|
..write(obj.serverUrl)
|
||||||
|
..writeByte(3)
|
||||||
|
..write(obj.isSaveLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => typeId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is HiveSavedLoginInfoAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
|
}
|
||||||
@@ -4,6 +4,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/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/login_response.model.dart';
|
import 'package:immich_mobile/modules/login/models/login_response.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/backup.service.dart';
|
import 'package:immich_mobile/shared/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
||||||
@@ -36,7 +37,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
final BackupService _backupService = BackupService();
|
final BackupService _backupService = BackupService();
|
||||||
final NetworkService _networkService = NetworkService();
|
final NetworkService _networkService = NetworkService();
|
||||||
|
|
||||||
Future<bool> login(String email, String password, String serverEndpoint) 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);
|
||||||
@@ -76,6 +77,20 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
userId: payload.userId,
|
userId: payload.userId,
|
||||||
userEmail: payload.userEmail,
|
userEmail: payload.userEmail,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isSavedLoginInfo) {
|
||||||
|
// Save login info to local storage
|
||||||
|
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put(
|
||||||
|
savedLoginInfoKey,
|
||||||
|
HiveSavedLoginInfo(
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
isSaveLogin: true,
|
||||||
|
serverUrl: Hive.box(userInfoBox).get(serverEndpointKey)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package: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/modules/login/models/hive_saved_login_info.model.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/shared/providers/backup.provider.dart';
|
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
||||||
@@ -12,22 +15,36 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final usernameController = useTextEditingController(text: 'testuser@email.com');
|
final usernameController = useTextEditingController.fromValue(TextEditingValue.empty);
|
||||||
final passwordController = useTextEditingController(text: 'password');
|
final passwordController = useTextEditingController.fromValue(TextEditingValue.empty);
|
||||||
final serverEndpointController = useTextEditingController(text: 'http://192.168.1.216:2283');
|
final serverEndpointController = useTextEditingController(text: 'http://your-server-ip:2283');
|
||||||
|
final isSaveLoginInfo = useState<bool>(false);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
|
||||||
|
|
||||||
|
if (loginInfo != null) {
|
||||||
|
usernameController.text = loginInfo.email;
|
||||||
|
passwordController.text = loginInfo.password;
|
||||||
|
serverEndpointController.text = loginInfo.serverUrl;
|
||||||
|
isSaveLoginInfo.value = loginInfo.isSaveLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 300),
|
constraints: const BoxConstraints(maxWidth: 300),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 32,
|
spacing: 16,
|
||||||
runSpacing: 32,
|
runSpacing: 16,
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Image(
|
const Image(
|
||||||
image: AssetImage('assets/immich-logo-no-outline.png'),
|
image: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
width: 128,
|
width: 100,
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -42,10 +59,29 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
EmailInput(controller: usernameController),
|
EmailInput(controller: usernameController),
|
||||||
PasswordInput(controller: passwordController),
|
PasswordInput(controller: passwordController),
|
||||||
ServerEndpointInput(controller: serverEndpointController),
|
ServerEndpointInput(controller: serverEndpointController),
|
||||||
|
CheckboxListTile(
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
dense: true,
|
||||||
|
side: const BorderSide(color: Colors.grey, width: 1.5),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
|
enableFeedback: true,
|
||||||
|
title: const Text(
|
||||||
|
"Save login",
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey),
|
||||||
|
),
|
||||||
|
value: isSaveLoginInfo.value,
|
||||||
|
onChanged: (switchValue) {
|
||||||
|
if (switchValue != null) {
|
||||||
|
isSaveLoginInfo.value = switchValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
LoginButton(
|
LoginButton(
|
||||||
emailController: usernameController,
|
emailController: usernameController,
|
||||||
passwordController: passwordController,
|
passwordController: passwordController,
|
||||||
serverEndpointController: serverEndpointController,
|
serverEndpointController: serverEndpointController,
|
||||||
|
isSavedLoginInfo: isSaveLoginInfo.value,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -104,29 +140,34 @@ class LoginButton extends ConsumerWidget {
|
|||||||
final TextEditingController emailController;
|
final TextEditingController emailController;
|
||||||
final TextEditingController passwordController;
|
final TextEditingController passwordController;
|
||||||
final TextEditingController serverEndpointController;
|
final TextEditingController serverEndpointController;
|
||||||
|
final bool isSavedLoginInfo;
|
||||||
|
|
||||||
const LoginButton(
|
const LoginButton({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.emailController,
|
required this.emailController,
|
||||||
required this.passwordController,
|
required this.passwordController,
|
||||||
required this.serverEndpointController})
|
required this.serverEndpointController,
|
||||||
: super(key: key);
|
required this.isSavedLoginInfo,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
|
style: ButtonStyle(
|
||||||
|
visualDensity: VisualDensity.standard,
|
||||||
|
padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.symmetric(vertical: 10, horizontal: 25)),
|
||||||
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// This will remove current cache asset state of previous user login.
|
// This will remove current cache asset state of previous user login.
|
||||||
ref.watch(assetProvider.notifier).clearAllAsset();
|
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||||
|
|
||||||
var isAuthenticated = await ref
|
var isAuthenticated = await ref
|
||||||
.read(authenticationProvider.notifier)
|
.read(authenticationProvider.notifier)
|
||||||
.login(emailController.text, passwordController.text, serverEndpointController.text);
|
.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();
|
ref.watch(backupProvider.notifier).resumeBackup();
|
||||||
// AutoRouter.of(context).pushNamed("/home-page");
|
|
||||||
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
||||||
} else {
|
} else {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
@@ -136,6 +177,9 @@ class LoginButton extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text("Login"));
|
child: const Text(
|
||||||
|
"Login",
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class AlbumViewerPageState {
|
||||||
|
final bool isEditAlbum;
|
||||||
|
final String editTitleText;
|
||||||
|
AlbumViewerPageState({
|
||||||
|
required this.isEditAlbum,
|
||||||
|
required this.editTitleText,
|
||||||
|
});
|
||||||
|
|
||||||
|
AlbumViewerPageState copyWith({
|
||||||
|
bool? isEditAlbum,
|
||||||
|
String? editTitleText,
|
||||||
|
}) {
|
||||||
|
return AlbumViewerPageState(
|
||||||
|
isEditAlbum: isEditAlbum ?? this.isEditAlbum,
|
||||||
|
editTitleText: editTitleText ?? this.editTitleText,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
|
result.addAll({'isEditAlbum': isEditAlbum});
|
||||||
|
result.addAll({'editTitleText': editTitleText});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory AlbumViewerPageState.fromMap(Map<String, dynamic> map) {
|
||||||
|
return AlbumViewerPageState(
|
||||||
|
isEditAlbum: map['isEditAlbum'] ?? false,
|
||||||
|
editTitleText: map['editTitleText'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory AlbumViewerPageState.fromJson(String source) => AlbumViewerPageState.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is AlbumViewerPageState && other.isEditAlbum == isEditAlbum && other.editTitleText == editTitleText;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => isEditAlbum.hashCode ^ editTitleText.hashCode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/sharing/models/album_viewer_page_state.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
||||||
|
|
||||||
|
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||||
|
AlbumViewerNotifier(this.ref) : super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
|
||||||
|
|
||||||
|
final Ref ref;
|
||||||
|
|
||||||
|
void enableEditAlbum() {
|
||||||
|
state = state.copyWith(isEditAlbum: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void disableEditAlbum() {
|
||||||
|
state = state.copyWith(isEditAlbum: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setEditTitleText(String newTitle) {
|
||||||
|
state = state.copyWith(editTitleText: newTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void remoteEditTitleText() {
|
||||||
|
state = state.copyWith(editTitleText: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetState() {
|
||||||
|
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> changeAlbumTitle(String albumId, String ownerId, String newAlbumTitle) async {
|
||||||
|
SharedAlbumService service = SharedAlbumService();
|
||||||
|
|
||||||
|
bool isSuccess = await service.changeTitleAlbum(albumId, ownerId, newAlbumTitle);
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||||
|
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final albumViewerProvider = StateNotifierProvider<AlbumViewerNotifier, AlbumViewerPageState>((ref) {
|
||||||
|
return AlbumViewerNotifier(ref);
|
||||||
|
});
|
||||||
@@ -138,4 +138,23 @@ class SharedAlbumService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> changeTitleAlbum(String albumId, String ownerId, String newAlbumTitle) async {
|
||||||
|
try {
|
||||||
|
Response res = await _networkService.patchRequest(url: 'shared/updateInfo', data: {
|
||||||
|
"albumId": albumId,
|
||||||
|
"ownerId": ownerId,
|
||||||
|
"albumName": newAlbumTitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error deleteAlbum ${e.toString()}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:fluttertoast/fluttertoast.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
import 'package:immich_mobile/constants/immich_colors.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/modules/sharing/providers/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
@@ -27,6 +28,8 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable;
|
final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable;
|
||||||
final selectedAssetsInAlbum = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
final selectedAssetsInAlbum = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
||||||
|
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
||||||
|
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
||||||
|
|
||||||
void _onDeleteAlbumPressed(String albumId) async {
|
void _onDeleteAlbumPressed(String albumId) async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
@@ -152,6 +155,24 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
);
|
);
|
||||||
|
} else if (isEditAlbum) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
bool isSuccess =
|
||||||
|
await ref.watch(albumViewerProvider.notifier).changeAlbumTitle(albumId, userId, newAlbumTitle);
|
||||||
|
|
||||||
|
if (!isSuccess) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "Failed to change album title",
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.check_rounded),
|
||||||
|
splashRadius: 25,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () async => await AutoRouter.of(context).pop(),
|
onPressed: () async => await AutoRouter.of(context).pop(),
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.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/providers/album_viewer.provider.dart';
|
||||||
|
|
||||||
|
class AlbumViewerEditableTitle extends HookConsumerWidget {
|
||||||
|
final SharedAlbum albumInfo;
|
||||||
|
final FocusNode titleFocusNode;
|
||||||
|
const AlbumViewerEditableTitle({Key? key, required this.albumInfo, required this.titleFocusNode}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final titleTextEditController = useTextEditingController(text: albumInfo.albumName);
|
||||||
|
|
||||||
|
void onFocusModeChange() {
|
||||||
|
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
|
||||||
|
ref.watch(albumViewerProvider.notifier).setEditTitleText("Untitled");
|
||||||
|
titleTextEditController.text = "Untitled";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
titleFocusNode.addListener(onFocusModeChange);
|
||||||
|
return () {
|
||||||
|
titleFocusNode.removeListener(onFocusModeChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return TextField(
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value.isEmpty) {
|
||||||
|
} else {
|
||||||
|
ref.watch(albumViewerProvider.notifier).setEditTitleText(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focusNode: titleFocusNode,
|
||||||
|
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
controller: titleTextEditController,
|
||||||
|
onTap: () {
|
||||||
|
FocusScope.of(context).requestFocus(titleFocusNode);
|
||||||
|
|
||||||
|
ref.watch(albumViewerProvider.notifier).setEditTitleText(albumInfo.albumName);
|
||||||
|
ref.watch(albumViewerProvider.notifier).enableEditAlbum();
|
||||||
|
|
||||||
|
if (titleTextEditController.text == 'Untitled') {
|
||||||
|
titleTextEditController.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
suffixIcon: titleFocusNode.hasFocus
|
||||||
|
? IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
titleTextEditController.clear();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.cancel_rounded),
|
||||||
|
splashRadius: 10,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderSide: const BorderSide(color: Colors.transparent),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderSide: const BorderSide(color: Colors.transparent),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
focusColor: Colors.grey[300],
|
||||||
|
fillColor: Colors.grey[200],
|
||||||
|
filled: titleFocusNode.hasFocus,
|
||||||
|
hintText: 'Add a title',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ 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';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/album_action_outlined_button.dart';
|
import 'package:immich_mobile/modules/sharing/ui/album_action_outlined_button.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/album_viewer_appbar.dart';
|
import 'package:immich_mobile/modules/sharing/ui/album_viewer_appbar.dart';
|
||||||
|
import 'package:immich_mobile/modules/sharing/ui/album_viewer_editable_title.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/album_viewer_thumbnail.dart';
|
import 'package:immich_mobile/modules/sharing/ui/album_viewer_thumbnail.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
@@ -26,6 +27,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
FocusNode titleFocusNode = useFocusNode();
|
||||||
ScrollController _scrollController = useScrollController();
|
ScrollController _scrollController = useScrollController();
|
||||||
AsyncValue<SharedAlbum> _albumInfo = ref.watch(sharedAlbumDetailProvider(albumId));
|
AsyncValue<SharedAlbum> _albumInfo = ref.watch(sharedAlbumDetailProvider(albumId));
|
||||||
|
|
||||||
@@ -83,13 +85,18 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTitle(String title) {
|
Widget _buildTitle(SharedAlbum albumInfo) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 16),
|
padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
|
||||||
child: Text(
|
child: userId == albumInfo.ownerId
|
||||||
title,
|
? AlbumViewerEditableTitle(
|
||||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
albumInfo: albumInfo,
|
||||||
),
|
titleFocusNode: titleFocusNode,
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: Text(albumInfo.albumName, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +131,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildTitle(albumInfo.albumName),
|
_buildTitle(albumInfo),
|
||||||
_buildAlbumDateRange(albumInfo),
|
_buildAlbumDateRange(albumInfo),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 60,
|
height: 60,
|
||||||
@@ -204,31 +211,36 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody(SharedAlbum albumInfo) {
|
Widget _buildBody(SharedAlbum albumInfo) {
|
||||||
return Stack(children: [
|
return GestureDetector(
|
||||||
DraggableScrollbar.semicircle(
|
onTap: () {
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
titleFocusNode.unfocus();
|
||||||
controller: _scrollController,
|
},
|
||||||
heightScrollThumb: 48.0,
|
child: Stack(children: [
|
||||||
child: CustomScrollView(
|
DraggableScrollbar.semicircle(
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
slivers: [
|
heightScrollThumb: 48.0,
|
||||||
_buildHeader(albumInfo),
|
child: CustomScrollView(
|
||||||
SliverPersistentHeader(
|
controller: _scrollController,
|
||||||
pinned: true,
|
slivers: [
|
||||||
delegate: ImmichSliverPersistentAppBarDelegate(
|
_buildHeader(albumInfo),
|
||||||
minHeight: 50,
|
SliverPersistentHeader(
|
||||||
maxHeight: 50,
|
pinned: true,
|
||||||
child: Container(
|
delegate: ImmichSliverPersistentAppBarDelegate(
|
||||||
color: immichBackgroundColor,
|
minHeight: 50,
|
||||||
child: _buildControlButton(albumInfo),
|
maxHeight: 50,
|
||||||
|
child: Container(
|
||||||
|
color: immichBackgroundColor,
|
||||||
|
child: _buildControlButton(albumInfo),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
_buildImageGrid(albumInfo)
|
||||||
_buildImageGrid(albumInfo)
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
]),
|
||||||
]);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.7.0+11
|
version: 1.8.0+12
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.15.1 <3.0.0"
|
sdk: ">=2.15.1 <3.0.0"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Headers,
|
Headers,
|
||||||
Delete,
|
Delete,
|
||||||
Logger,
|
Logger,
|
||||||
|
Patch,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
|
|||||||
12
server/src/api-v1/sharing/dto/update-shared-album.dto.ts
Normal file
12
server/src/api-v1/sharing/dto/update-shared-album.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateShareAlbumDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
albumId: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
albumName: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
ownerId: string;
|
||||||
|
}
|
||||||
@@ -2,10 +2,11 @@ import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Validatio
|
|||||||
import { SharingService } from './sharing.service';
|
import { SharingService } from './sharing.service';
|
||||||
import { CreateSharedAlbumDto } from './dto/create-shared-album.dto';
|
import { CreateSharedAlbumDto } from './dto/create-shared-album.dto';
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { GetAuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
import { AddAssetsDto } from './dto/add-assets.dto';
|
||||||
import { AddUsersDto } from './dto/add-users.dto';
|
import { AddUsersDto } from './dto/add-users.dto';
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||||
|
import { UpdateShareAlbumDto } from './dto/update-shared-album.dto';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('shared')
|
@Controller('shared')
|
||||||
@@ -52,4 +53,9 @@ export class SharingController {
|
|||||||
async leaveAlbum(@GetAuthUser() authUser, @Param('albumId') albumId: string) {
|
async leaveAlbum(@GetAuthUser() authUser, @Param('albumId') albumId: string) {
|
||||||
return await this.sharingService.leaveAlbum(authUser, albumId);
|
return await this.sharingService.leaveAlbum(authUser, albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch('/updateInfo')
|
||||||
|
async updateAlbumInfo(@GetAuthUser() authUser, @Body(ValidationPipe) updateAlbumInfoDto: UpdateShareAlbumDto) {
|
||||||
|
return await this.sharingService.updateAlbumTitle(authUser, updateAlbumInfoDto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { UserSharedAlbumEntity } from './entities/user-shared-album.entity';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { AddUsersDto } from './dto/add-users.dto';
|
import { AddUsersDto } from './dto/add-users.dto';
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||||
|
import { UpdateShareAlbumDto } from './dto/update-shared-album.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SharingService {
|
export class SharingService {
|
||||||
@@ -184,4 +185,15 @@ export class SharingService {
|
|||||||
|
|
||||||
return await this.assetSharedAlbumRepository.save([...newRecords]);
|
return await this.assetSharedAlbumRepository.save([...newRecords]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateAlbumTitle(authUser: AuthUserDto, updateShareAlbumDto: UpdateShareAlbumDto) {
|
||||||
|
if (authUser.id != updateShareAlbumDto.ownerId) {
|
||||||
|
throw new BadRequestException('Unauthorized to change album info');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharedAlbum = await this.sharedAlbumRepository.findOne({ where: { id: updateShareAlbumDto.albumId } });
|
||||||
|
sharedAlbum.albumName = updateShareAlbumDto.albumName;
|
||||||
|
|
||||||
|
return await this.sharedAlbumRepository.save(sharedAlbum);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
export const serverVersion = {
|
export const serverVersion = {
|
||||||
major: 1,
|
major: 1,
|
||||||
minor: 7,
|
minor: 8,
|
||||||
patch: 0,
|
patch: 0,
|
||||||
build: 11,
|
build: 12,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user