Compare commits
24 Commits
first-andr
...
v0.2-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b04e69fd66 | ||
|
|
0a1e28a08f | ||
|
|
9fdaa82d77 | ||
|
|
88123b1cd2 | ||
|
|
42c4c9dba1 | ||
|
|
e63dc49475 | ||
|
|
690f30f3dd | ||
|
|
561b030e80 | ||
|
|
4756c075b6 | ||
|
|
328f382f86 | ||
|
|
6ad77e9434 | ||
|
|
919928ab70 | ||
|
|
2a4d4ea999 | ||
|
|
547ce49500 | ||
|
|
f4970ed053 | ||
|
|
9cf083decf | ||
|
|
d078367c04 | ||
|
|
a8edc85183 | ||
|
|
5d48de7fa9 | ||
|
|
82beb040bc | ||
|
|
03864e52ff | ||
|
|
c24fb403c5 | ||
|
|
1d3ee2008c | ||
|
|
c917875943 |
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Maintain dependencies for GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
46
.github/workflows/Build+push Immich.yml
vendored
Normal file
46
.github/workflows/Build+push Immich.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: Build+push Immich
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Triggers the workflow on push or pull request events but only for the main branch
|
||||||
|
#schedule:
|
||||||
|
# * is a special character in YAML so you have to quote this string
|
||||||
|
#- cron: '0 0 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
buildandpush:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2.4.0
|
||||||
|
with:
|
||||||
|
ref: "main" # branch
|
||||||
|
# https://github.com/docker/setup-qemu-action#usage
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1.2.0
|
||||||
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1.6.0
|
||||||
|
# https://github.com/docker/login-action#docker-hub
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
|
- name: Build and push Immich
|
||||||
|
uses: docker/build-push-action@v2.9.0
|
||||||
|
with:
|
||||||
|
context: ./server
|
||||||
|
file: ./server/Dockerfile
|
||||||
|
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
||||||
|
platforms: linux/arm/v7,linux/amd64
|
||||||
|
pull: true
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
altran1502/immich-server:latest
|
||||||
32
.github/workflows/build_apk.yml
vendored
Normal file
32
.github/workflows/build_apk.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Build APK Android
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./mobile
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Build
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: "stable"
|
||||||
|
cache: true
|
||||||
|
cache-key: flutter2.10 # optional, change this to force refresh cache
|
||||||
|
- run: flutter --version
|
||||||
|
- run: flutter pub get
|
||||||
|
- run: flutter build apk
|
||||||
|
- run: flutter build appbundle
|
||||||
|
|
||||||
|
# Upload Artifact
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: release-apk
|
||||||
|
path: mobile/build/app/outputs/apk/release/app-release.apk
|
||||||
28
README.md
28
README.md
@@ -4,7 +4,21 @@
|
|||||||
|
|
||||||
# IMMICH
|
# IMMICH
|
||||||
|
|
||||||
Self-hosted Photo backup solution directly from your mobile phone.
|
Self-hosted photo and video backup solution directly from your mobile phone.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Loading ~4000 images/videos
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
<p align="left">
|
||||||
|
<img src="design/sc1.PNG" width="150" title="Login With Custom URL">
|
||||||
|
<img src="design/sc2.PNG" width="150" title="Backup Setting Info">
|
||||||
|
<img src="design/sc4.PNG" width="150" title="Home Page">
|
||||||
|
<img src="design/sc3.PNG" width="150" title="Multiple seelct">
|
||||||
|
<img src="design/sc5.PNG" width="150" title="Multipe select group">
|
||||||
|
</p>
|
||||||
|
|
||||||
# Note
|
# Note
|
||||||
|
|
||||||
@@ -12,6 +26,16 @@ This project is under heavy development, there will be continous functions, feat
|
|||||||
|
|
||||||
**!! NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS !!**
|
**!! NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS !!**
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
[x] Upload assets(videos/images)
|
||||||
|
|
||||||
|
[x] View assets
|
||||||
|
|
||||||
|
[x] Quick navigation with drag scroll bar
|
||||||
|
|
||||||
|
[x] Auto Backup
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
You can use docker compose for development, there are several services that compose Immich
|
You can use docker compose for development, there are several services that compose Immich
|
||||||
@@ -79,7 +103,7 @@ flutter run --release
|
|||||||
|
|
||||||
# Known Issue
|
# Known Issue
|
||||||
|
|
||||||
TensorFlow doesn't run with older CPU architecture, it requires CPU with AVX and AVX2 instruction set. If you encounter error `illegal instruction core dump` when running the docker-compose command above, check for your CPU flags with the command ad make sure you see `AVX` and `AVX2`. Otherwise, switch to a different VM/desktop with different architecture.
|
TensorFlow doesn't run with older CPU architecture, it requires CPU with AVX and AVX2 instruction set. If you encounter the error `illegal instruction core dump` when running the docker-compose command above, check for your CPU flags with the command and make sure you see `AVX` and `AVX2`. Otherwise, switch to a different VM/desktop with different architecture.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
more /proc/cpuinfo | grep flags
|
more /proc/cpuinfo | grep flags
|
||||||
|
|||||||
BIN
design/sc1.PNG
Normal file
BIN
design/sc1.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 260 KiB |
BIN
design/sc2.PNG
Normal file
BIN
design/sc2.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 345 KiB |
BIN
design/sc3.PNG
Normal file
BIN
design/sc3.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 765 KiB |
BIN
design/sc4.PNG
Normal file
BIN
design/sc4.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 734 KiB |
BIN
design/sc5.PNG
Normal file
BIN
design/sc5.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 583 KiB |
@@ -1,16 +1 @@
|
|||||||
# immich_mobile
|
# Immich Mobile Application - Flutter
|
||||||
|
|
||||||
A new Flutter project.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
This project is a starting point for a Flutter application.
|
|
||||||
|
|
||||||
Few resources to get you started if this is your first Flutter project:
|
|
||||||
|
|
||||||
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
|
|
||||||
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
|
|
||||||
|
|
||||||
For help getting started with Flutter, view our
|
|
||||||
[online documentation](https://flutter.dev/docs), which offers tutorials,
|
|
||||||
samples, guidance on mobile development, and a full API reference.
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
||||||
import 'constants/hive_box.dart';
|
import 'constants/hive_box.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
case AppLifecycleState.resumed:
|
case AppLifecycleState.resumed:
|
||||||
debugPrint("[APP STATE] resumed");
|
debugPrint("[APP STATE] resumed");
|
||||||
ref.read(appStateProvider.notifier).state = AppStateEnum.resumed;
|
ref.read(appStateProvider.notifier).state = AppStateEnum.resumed;
|
||||||
|
ref.read(backupProvider.notifier).resumeBackup();
|
||||||
break;
|
break;
|
||||||
case AppLifecycleState.inactive:
|
case AppLifecycleState.inactive:
|
||||||
debugPrint("[APP STATE] inactive");
|
debugPrint("[APP STATE] inactive");
|
||||||
@@ -53,7 +55,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initApp() async {
|
Future<void> initApp() async {
|
||||||
// WidgetsBinding.instance?.addObserver(this);
|
WidgetsBinding.instance?.addObserver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
66
mobile/lib/modules/home/models/home_page_state.model.dart
Normal file
66
mobile/lib/modules/home/models/home_page_state.model.dart
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
|
|
||||||
|
class HomePageState {
|
||||||
|
final bool isMultiSelectEnable;
|
||||||
|
final Set<ImmichAsset> selectedItems;
|
||||||
|
final Set<String> selectedDateGroup;
|
||||||
|
HomePageState({
|
||||||
|
required this.isMultiSelectEnable,
|
||||||
|
required this.selectedItems,
|
||||||
|
required this.selectedDateGroup,
|
||||||
|
});
|
||||||
|
|
||||||
|
HomePageState copyWith({
|
||||||
|
bool? isMultiSelectEnable,
|
||||||
|
Set<ImmichAsset>? selectedItems,
|
||||||
|
Set<String>? selectedDateGroup,
|
||||||
|
}) {
|
||||||
|
return HomePageState(
|
||||||
|
isMultiSelectEnable: isMultiSelectEnable ?? this.isMultiSelectEnable,
|
||||||
|
selectedItems: selectedItems ?? this.selectedItems,
|
||||||
|
selectedDateGroup: selectedDateGroup ?? this.selectedDateGroup,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'isMultiSelectEnable': isMultiSelectEnable,
|
||||||
|
'selectedItems': selectedItems.map((x) => x.toMap()).toList(),
|
||||||
|
'selectedDateGroup': selectedDateGroup.toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory HomePageState.fromMap(Map<String, dynamic> map) {
|
||||||
|
return HomePageState(
|
||||||
|
isMultiSelectEnable: map['isMultiSelectEnable'] ?? false,
|
||||||
|
selectedItems: Set<ImmichAsset>.from(map['selectedItems']?.map((x) => ImmichAsset.fromMap(x))),
|
||||||
|
selectedDateGroup: Set<String>.from(map['selectedDateGroup']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory HomePageState.fromJson(String source) => HomePageState.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
final setEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
|
return other is HomePageState &&
|
||||||
|
other.isMultiSelectEnable == isMultiSelectEnable &&
|
||||||
|
setEquals(other.selectedItems, selectedItems) &&
|
||||||
|
setEquals(other.selectedDateGroup, selectedDateGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => isMultiSelectEnable.hashCode ^ selectedItems.hashCode ^ selectedDateGroup.hashCode;
|
||||||
|
}
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
|
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
class AssetNotifier extends StateNotifier<List<ImmichAssetGroupByDate>> {
|
class AssetNotifier extends StateNotifier<List<ImmichAssetGroupByDate>> {
|
||||||
final imagePerPage = 100;
|
|
||||||
final AssetService _assetService = AssetService();
|
final AssetService _assetService = AssetService();
|
||||||
|
|
||||||
AssetNotifier() : super([]);
|
AssetNotifier() : super([]);
|
||||||
|
|
||||||
late String? nextPageKey = "";
|
late String? nextPageKey = "";
|
||||||
bool isFetching = false;
|
bool isFetching = false;
|
||||||
|
|
||||||
|
// Get All assets
|
||||||
getImmichAssets() async {
|
getImmichAssets() async {
|
||||||
GetAllAssetResponse? res = await _assetService.getAllAsset();
|
GetAllAssetResponse? res = await _assetService.getAllAsset();
|
||||||
nextPageKey = res?.nextPageKey;
|
nextPageKey = res?.nextPageKey;
|
||||||
@@ -21,10 +25,11 @@ class AssetNotifier extends StateNotifier<List<ImmichAssetGroupByDate>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getMoreAsset() async {
|
// Get Asset From The Past
|
||||||
|
getOlderAsset() async {
|
||||||
if (nextPageKey != null && !isFetching) {
|
if (nextPageKey != null && !isFetching) {
|
||||||
isFetching = true;
|
isFetching = true;
|
||||||
GetAllAssetResponse? res = await _assetService.getMoreAsset(nextPageKey);
|
GetAllAssetResponse? res = await _assetService.getOlderAsset(nextPageKey);
|
||||||
|
|
||||||
if (res != null) {
|
if (res != null) {
|
||||||
nextPageKey = res.nextPageKey;
|
nextPageKey = res.nextPageKey;
|
||||||
@@ -48,6 +53,40 @@ class AssetNotifier extends StateNotifier<List<ImmichAssetGroupByDate>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get newer asset from the current time
|
||||||
|
getNewAsset() async {
|
||||||
|
if (state.isNotEmpty) {
|
||||||
|
var latestGroup = state.first;
|
||||||
|
|
||||||
|
// Sort the last asset group and put the lastest asset in front.
|
||||||
|
latestGroup.assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
||||||
|
var latestAsset = latestGroup.assets.first;
|
||||||
|
var formatDateTemplate = 'y-MM-dd';
|
||||||
|
var latestAssetDateText = DateFormat(formatDateTemplate).format(DateTime.parse(latestAsset.createdAt));
|
||||||
|
|
||||||
|
List<ImmichAsset> newAssets = await _assetService.getNewAsset(latestAsset.createdAt);
|
||||||
|
|
||||||
|
if (newAssets.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grouping by data
|
||||||
|
var groupByDateList = groupBy<ImmichAsset, String>(
|
||||||
|
newAssets, (asset) => DateFormat(formatDateTemplate).format(DateTime.parse(asset.createdAt)));
|
||||||
|
|
||||||
|
groupByDateList.forEach((groupDateInFormattedText, assets) {
|
||||||
|
if (groupDateInFormattedText != latestAssetDateText) {
|
||||||
|
ImmichAssetGroupByDate newGroup = ImmichAssetGroupByDate(assets: assets, date: groupDateInFormattedText);
|
||||||
|
state = [newGroup, ...state];
|
||||||
|
} else {
|
||||||
|
latestGroup.assets.insertAll(0, assets);
|
||||||
|
|
||||||
|
state = [latestGroup, ...state.sublist(1)];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
clearAllAsset() {
|
clearAllAsset() {
|
||||||
state = [];
|
state = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
|
|
||||||
|
class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
||||||
|
HomePageStateNotifier()
|
||||||
|
: super(
|
||||||
|
HomePageState(
|
||||||
|
isMultiSelectEnable: false,
|
||||||
|
selectedItems: {},
|
||||||
|
selectedDateGroup: {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
void addSelectedDateGroup(String dateGroupTitle) {
|
||||||
|
state = state.copyWith(selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle});
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeSelectedDateGroup(String dateGroupTitle) {
|
||||||
|
var currentDateGroup = state.selectedDateGroup;
|
||||||
|
|
||||||
|
currentDateGroup.removeWhere((e) => e == dateGroupTitle);
|
||||||
|
|
||||||
|
state = state.copyWith(selectedDateGroup: currentDateGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
void enableMultiSelect(Set<ImmichAsset> selectedItems) {
|
||||||
|
state = state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
void disableMultiSelect() {
|
||||||
|
state = state.copyWith(isMultiSelectEnable: false, selectedItems: {}, selectedDateGroup: {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void addSingleSelectedItem(ImmichAsset asset) {
|
||||||
|
state = state.copyWith(selectedItems: {...state.selectedItems, asset});
|
||||||
|
}
|
||||||
|
|
||||||
|
void addMultipleSelectedItems(List<ImmichAsset> assets) {
|
||||||
|
state = state.copyWith(selectedItems: {...state.selectedItems, ...assets});
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeSingleSelectedItem(ImmichAsset asset) {
|
||||||
|
Set<ImmichAsset> currentList = state.selectedItems;
|
||||||
|
|
||||||
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
|
|
||||||
|
state = state.copyWith(selectedItems: currentList);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeMultipleSelectedItem(List<ImmichAsset> assets) {
|
||||||
|
Set<ImmichAsset> currentList = state.selectedItems;
|
||||||
|
|
||||||
|
for (ImmichAsset asset in assets) {
|
||||||
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(selectedItems: currentList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final homePageStateProvider =
|
||||||
|
StateNotifierProvider<HomePageStateNotifier, HomePageState>(((ref) => HomePageStateNotifier()));
|
||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
|
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.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';
|
||||||
|
|
||||||
class AssetService {
|
class AssetService {
|
||||||
@@ -17,9 +18,10 @@ class AssetService {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
debugPrint("Error getAllAsset ${e.toString()}");
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GetAllAssetResponse?> getMoreAsset(String? nextPageKey) async {
|
Future<GetAllAssetResponse?> getOlderAsset(String? nextPageKey) async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(
|
var res = await _networkService.getRequest(
|
||||||
url: "asset/all?nextPageKey=$nextPageKey",
|
url: "asset/all?nextPageKey=$nextPageKey",
|
||||||
@@ -34,5 +36,26 @@ class AssetService {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
debugPrint("Error getAllAsset ${e.toString()}");
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ImmichAsset>> getNewAsset(String latestDate) async {
|
||||||
|
try {
|
||||||
|
var res = await _networkService.getRequest(
|
||||||
|
url: "asset/new?latestDate=$latestDate",
|
||||||
|
);
|
||||||
|
|
||||||
|
List<dynamic> decodedData = jsonDecode(res.toString());
|
||||||
|
|
||||||
|
List<ImmichAsset> result = List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
|
||||||
|
if (result.isNotEmpty) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error getAllAsset ${e.toString()}");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
75
mobile/lib/modules/home/ui/daily_title_text.dart
Normal file
75
mobile/lib/modules/home/ui/daily_title_text.dart
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class DailyTitleText extends ConsumerWidget {
|
||||||
|
const DailyTitleText({
|
||||||
|
Key? key,
|
||||||
|
required this.isoDate,
|
||||||
|
required this.assetGroup,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String isoDate;
|
||||||
|
final List<ImmichAsset> assetGroup;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
var currentYear = DateTime.now().year;
|
||||||
|
var groupYear = DateTime.parse(isoDate).year;
|
||||||
|
var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
|
||||||
|
var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
|
||||||
|
var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||||
|
var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
|
||||||
|
var selectedItems = ref.watch(homePageStateProvider).selectedItems;
|
||||||
|
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 29.0, bottom: 29.0, left: 12.0, right: 12.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
dateText,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (isMultiSelectEnable &&
|
||||||
|
selectedDateGroup.contains(dateText) &&
|
||||||
|
selectedDateGroup.length == 1 &&
|
||||||
|
selectedItems.length == assetGroup.length) {
|
||||||
|
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
||||||
|
} else if (isMultiSelectEnable &&
|
||||||
|
selectedDateGroup.contains(dateText) &&
|
||||||
|
selectedItems.length != assetGroup.length) {
|
||||||
|
ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText);
|
||||||
|
ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup);
|
||||||
|
} else if (isMultiSelectEnable &&
|
||||||
|
selectedDateGroup.contains(dateText) &&
|
||||||
|
selectedDateGroup.length > 1) {
|
||||||
|
ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText);
|
||||||
|
ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup);
|
||||||
|
} else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
|
||||||
|
ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText);
|
||||||
|
ref.watch(homePageStateProvider.notifier).addMultipleSelectedItems(assetGroup);
|
||||||
|
} else {
|
||||||
|
ref.watch(homePageStateProvider.notifier).enableMultiSelect(assetGroup.toSet());
|
||||||
|
ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: isMultiSelectEnable && selectedDateGroup.contains(dateText)
|
||||||
|
? const Icon(Icons.check_circle_rounded)
|
||||||
|
: const Icon(Icons.check_circle_outline_rounded),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
|
||||||
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/backup_state.model.dart';
|
import 'package:immich_mobile/shared/models/backup_state.model.dart';
|
||||||
@@ -12,9 +11,11 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
const ImmichSliverAppBar({
|
const ImmichSliverAppBar({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.imageGridGroup,
|
required this.imageGridGroup,
|
||||||
|
this.onPopBack,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final List<Widget> imageGridGroup;
|
final List<Widget> imageGridGroup;
|
||||||
|
final Function? onPopBack;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -75,15 +76,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
|
var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
|
||||||
|
|
||||||
if (onPop == true) {
|
if (onPop == true) {
|
||||||
// Remove and force getting new widget again if there is not many widget on screen.
|
onPopBack!();
|
||||||
// Otherwise do nothing.
|
|
||||||
if (imageGridGroup.isNotEmpty && imageGridGroup.length < 20) {
|
|
||||||
print("Get more access");
|
|
||||||
ref.read(assetProvider.notifier).getMoreAsset();
|
|
||||||
} else if (imageGridGroup.isEmpty) {
|
|
||||||
print("get immich asset");
|
|
||||||
ref.read(assetProvider.notifier).getImmichAssets();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
30
mobile/lib/modules/home/ui/monthly_title_text.dart
Normal file
30
mobile/lib/modules/home/ui/monthly_title_text.dart
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class MonthlyTitleText extends StatelessWidget {
|
||||||
|
const MonthlyTitleText({
|
||||||
|
Key? key,
|
||||||
|
required this.isoDate,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String isoDate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var monthTitleText = DateFormat('MMMM y').format(DateTime.parse(isoDate));
|
||||||
|
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 12.0, top: 32),
|
||||||
|
child: Text(
|
||||||
|
monthTitleText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
import 'package:auto_route/annotations.dart';
|
|
||||||
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/src/widgets/framework.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/asset.provider.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/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
|
|
||||||
class ProfileDrawer extends ConsumerWidget {
|
class ProfileDrawer extends ConsumerWidget {
|
||||||
const ProfileDrawer({Key? key}) : super(key: key);
|
const ProfileDrawer({Key? key}) : super(key: key);
|
||||||
@@ -58,10 +55,10 @@ class ProfileDrawer extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
bool res = await ref.read(authenticationProvider.notifier).logout();
|
bool res = await ref.read(authenticationProvider.notifier).logout();
|
||||||
ref.read(assetProvider.notifier).clearAllAsset();
|
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
AutoRouter.of(context).popUntilRoot();
|
AutoRouter.of(context).popUntilRoot();
|
||||||
|
ref.read(assetProvider.notifier).clearAllAsset();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,66 +1,121 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.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/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
class ThumbnailImage extends HookWidget {
|
class ThumbnailImage extends HookConsumerWidget {
|
||||||
final ImmichAsset asset;
|
final ImmichAsset asset;
|
||||||
|
|
||||||
const ThumbnailImage({Key? key, required this.asset}) : super(key: key);
|
const ThumbnailImage({Key? key, required this.asset}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final cacheKey = useState(1);
|
final cacheKey = useState(1);
|
||||||
|
|
||||||
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(homePageStateProvider).selectedItems;
|
||||||
|
var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||||
|
|
||||||
|
Widget _buildSelectionIcon(ImmichAsset asset) {
|
||||||
|
if (selectedAsset.contains(asset)) {
|
||||||
|
return Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const Icon(
|
||||||
|
Icons.circle_outlined,
|
||||||
|
color: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (asset.type == 'IMAGE') {
|
if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length == 1) {
|
||||||
AutoRouter.of(context).push(
|
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
||||||
ImageViewerRoute(
|
} else if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length > 1) {
|
||||||
imageUrl:
|
ref.watch(homePageStateProvider.notifier).removeSingleSelectedItem(asset);
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false',
|
} else if (isMultiSelectEnable && !selectedAsset.contains(asset)) {
|
||||||
heroTag: asset.id,
|
ref.watch(homePageStateProvider.notifier).addSingleSelectedItem(asset);
|
||||||
thumbnailUrl: thumbnailRequestUrl,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
debugPrint("Navigate to video player");
|
if (asset.type == 'IMAGE') {
|
||||||
|
AutoRouter.of(context).push(
|
||||||
|
ImageViewerRoute(
|
||||||
|
imageUrl:
|
||||||
|
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false',
|
||||||
|
heroTag: asset.id,
|
||||||
|
thumbnailUrl: thumbnailRequestUrl,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint("Navigate to video player");
|
||||||
|
|
||||||
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}',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress: () {},
|
onLongPress: () {
|
||||||
|
// Enable multi selecte function
|
||||||
|
ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset});
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
},
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: asset.id,
|
tag: asset.id,
|
||||||
child: CachedNetworkImage(
|
child: Stack(
|
||||||
cacheKey: "${asset.id}-${cacheKey.value}",
|
children: [
|
||||||
width: 300,
|
Container(
|
||||||
height: 300,
|
decoration: BoxDecoration(
|
||||||
memCacheHeight: asset.type == 'IMAGE' ? 250 : 400,
|
border: isMultiSelectEnable && selectedAsset.contains(asset)
|
||||||
fit: BoxFit.cover,
|
? Border.all(color: Theme.of(context).primaryColorLight, width: 10)
|
||||||
imageUrl: thumbnailRequestUrl,
|
: const Border(),
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
),
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
child: CachedNetworkImage(
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
|
cacheKey: "${asset.id}-${cacheKey.value}",
|
||||||
scale: 0.2,
|
width: 300,
|
||||||
child: CircularProgressIndicator(value: downloadProgress.progress),
|
height: 300,
|
||||||
),
|
memCacheHeight: asset.type == 'IMAGE' ? 250 : 400,
|
||||||
errorWidget: (context, url, error) {
|
fit: BoxFit.cover,
|
||||||
debugPrint("Error Loading Thumbnail Widget $error");
|
imageUrl: thumbnailRequestUrl,
|
||||||
cacheKey.value += 1;
|
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||||
return const Icon(Icons.error);
|
fadeInDuration: const Duration(milliseconds: 250),
|
||||||
},
|
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
|
||||||
|
scale: 0.2,
|
||||||
|
child: CircularProgressIndicator(value: downloadProgress.progress),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) {
|
||||||
|
debugPrint("Error Loading Thumbnail Widget $error");
|
||||||
|
cacheKey.value += 1;
|
||||||
|
return const Icon(Icons.error);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
child: isMultiSelectEnable
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.all(3.0),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: _buildSelectionIcon(asset),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,36 +1,29 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
|
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
|
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
|
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
|
||||||
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
|
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
class HomePage extends HookConsumerWidget {
|
class HomePage extends HookConsumerWidget {
|
||||||
const HomePage({Key? key}) : super(key: key);
|
const HomePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final ValueNotifier<bool> _showBackToTopBtn = useState(false);
|
|
||||||
ScrollController _scrollController = useScrollController();
|
ScrollController _scrollController = useScrollController();
|
||||||
|
List<ImmichAssetGroupByDate> _assetGroup = ref.watch(assetProvider);
|
||||||
List<ImmichAssetGroupByDate> assetGroup = ref.watch(assetProvider);
|
List<Widget> _imageGridGroup = [];
|
||||||
List<Widget> imageGridGroup = [];
|
|
||||||
|
|
||||||
_scrollControllerCallback() {
|
_scrollControllerCallback() {
|
||||||
var endOfPage = _scrollController.position.maxScrollExtent;
|
var endOfPage = _scrollController.position.maxScrollExtent;
|
||||||
|
|
||||||
if (_scrollController.offset >= endOfPage - (endOfPage * 0.1) && !_scrollController.position.outOfRange) {
|
if (_scrollController.offset >= endOfPage - (endOfPage * 0.1) && !_scrollController.position.outOfRange) {
|
||||||
ref.read(assetProvider.notifier).getMoreAsset();
|
ref.read(assetProvider.notifier).getOlderAsset();
|
||||||
}
|
|
||||||
|
|
||||||
if (_scrollController.offset >= 400) {
|
|
||||||
_showBackToTopBtn.value = true;
|
|
||||||
} else {
|
|
||||||
_showBackToTopBtn.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,11 +37,23 @@ class HomePage extends HookConsumerWidget {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
Widget _buildBody() {
|
onPopBackFromBackupPage() {
|
||||||
if (assetGroup.isNotEmpty) {
|
ref.read(assetProvider.notifier).getNewAsset();
|
||||||
String lastGroupDate = assetGroup[0].date;
|
// Remove and force getting new widget again if there is not many widget on screen.
|
||||||
|
// Otherwise do nothing.
|
||||||
|
|
||||||
for (var group in assetGroup) {
|
if (_imageGridGroup.isNotEmpty && _imageGridGroup.length < 20) {
|
||||||
|
ref.read(assetProvider.notifier).getOlderAsset();
|
||||||
|
} else if (_imageGridGroup.isEmpty) {
|
||||||
|
ref.read(assetProvider.notifier).getImmichAssets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBody() {
|
||||||
|
if (_assetGroup.isNotEmpty) {
|
||||||
|
String lastGroupDate = _assetGroup[0].date;
|
||||||
|
|
||||||
|
for (var group in _assetGroup) {
|
||||||
var dateTitle = group.date;
|
var dateTitle = group.date;
|
||||||
var assetGroup = group.assets;
|
var assetGroup = group.assets;
|
||||||
|
|
||||||
@@ -56,19 +61,22 @@ class HomePage extends HookConsumerWidget {
|
|||||||
int? previousMonth = DateTime.tryParse(lastGroupDate)?.month;
|
int? previousMonth = DateTime.tryParse(lastGroupDate)?.month;
|
||||||
|
|
||||||
// Add Monthly Title Group if started at the beginning of the month
|
// Add Monthly Title Group if started at the beginning of the month
|
||||||
if ((currentMonth! - previousMonth!) != 0) {
|
|
||||||
imageGridGroup.add(
|
if (currentMonth != null && previousMonth != null) {
|
||||||
MonthlyTitleText(isoDate: dateTitle),
|
if ((currentMonth - previousMonth) != 0) {
|
||||||
);
|
_imageGridGroup.add(
|
||||||
|
MonthlyTitleText(isoDate: dateTitle),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Daily Title Group
|
// Add Daily Title Group
|
||||||
imageGridGroup.add(
|
_imageGridGroup.add(
|
||||||
DailyTitleText(isoDate: dateTitle),
|
DailyTitleText(isoDate: dateTitle, assetGroup: assetGroup),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add Image Group
|
// Add Image Group
|
||||||
imageGridGroup.add(
|
_imageGridGroup.add(
|
||||||
ImageGrid(assetGroup: assetGroup),
|
ImageGrid(assetGroup: assetGroup),
|
||||||
);
|
);
|
||||||
//
|
//
|
||||||
@@ -84,8 +92,11 @@ class HomePage extends HookConsumerWidget {
|
|||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
ImmichSliverAppBar(imageGridGroup: imageGridGroup),
|
ImmichSliverAppBar(
|
||||||
...imageGridGroup,
|
imageGridGroup: _imageGridGroup,
|
||||||
|
onPopBack: onPopBackFromBackupPage,
|
||||||
|
),
|
||||||
|
..._imageGridGroup,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -98,69 +109,3 @@ class HomePage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MonthlyTitleText extends StatelessWidget {
|
|
||||||
const MonthlyTitleText({
|
|
||||||
Key? key,
|
|
||||||
required this.isoDate,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final String isoDate;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var monthTitleText = DateFormat('MMMM, y').format(DateTime.parse(isoDate));
|
|
||||||
|
|
||||||
return SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 10.0, top: 32),
|
|
||||||
child: Text(
|
|
||||||
monthTitleText,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DailyTitleText extends StatelessWidget {
|
|
||||||
const DailyTitleText({
|
|
||||||
Key? key,
|
|
||||||
required this.isoDate,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final String isoDate;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var currentYear = DateTime.now().year;
|
|
||||||
var groupYear = DateTime.parse(isoDate).year;
|
|
||||||
var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
|
|
||||||
var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
|
|
||||||
|
|
||||||
return SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 24.0, bottom: 24.0, left: 3.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8.0, bottom: 5.0, top: 5.0),
|
|
||||||
child: Text(
|
|
||||||
dateText,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ 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()
|
AuthenticationNotifier(this.ref)
|
||||||
: super(
|
: super(
|
||||||
AuthenticationState(
|
AuthenticationState(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
@@ -31,6 +31,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final Ref ref;
|
||||||
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
||||||
final BackupService _backupService = BackupService();
|
final BackupService _backupService = BackupService();
|
||||||
final NetworkService _networkService = NetworkService();
|
final NetworkService _networkService = NetworkService();
|
||||||
@@ -126,5 +127,5 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
|
final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
|
||||||
return AuthenticationNotifier();
|
return AuthenticationNotifier(ref);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/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/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
|
||||||
class LoginForm extends HookConsumerWidget {
|
class LoginForm extends HookConsumerWidget {
|
||||||
@@ -110,11 +112,16 @@ class LoginButton extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
// This will remove current cache asset state of previous user login.
|
||||||
|
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||||
|
|
||||||
var isAuthenicated = await ref
|
var isAuthenicated = await ref
|
||||||
.read(authenticationProvider.notifier)
|
.read(authenticationProvider.notifier)
|
||||||
.login(emailController.text, passwordController.text, serverEndpointController.text);
|
.login(emailController.text, passwordController.text, serverEndpointController.text);
|
||||||
|
|
||||||
if (isAuthenicated) {
|
if (isAuthenicated) {
|
||||||
|
// Resume backup (if enable) then navigate
|
||||||
|
ref.watch(backupProvider.notifier).resumeBackup();
|
||||||
AutoRouter.of(context).pushNamed("/home-page");
|
AutoRouter.of(context).pushNamed("/home-page");
|
||||||
} else {
|
} else {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/backup_state.model.dart';
|
import 'package:immich_mobile/shared/models/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
||||||
@@ -8,7 +11,7 @@ import 'package:immich_mobile/shared/services/backup.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()
|
BackupNotifier(this.ref)
|
||||||
: super(
|
: super(
|
||||||
BackUpState(
|
BackUpState(
|
||||||
backupProgress: BackUpProgressEnum.idle,
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
@@ -29,6 +32,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final Ref ref;
|
||||||
final BackupService _backupService = BackupService();
|
final BackupService _backupService = BackupService();
|
||||||
final ServerInfoService _serverInfoService = ServerInfoService();
|
final ServerInfoService _serverInfoService = ServerInfoService();
|
||||||
|
|
||||||
@@ -96,7 +100,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
void cancelBackup() {
|
void cancelBackup() {
|
||||||
state.cancelToken.cancel('Cancel Backup');
|
state.cancelToken.cancel('Cancel Backup');
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
|
state = state.copyWith(backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAssetUploaded() {
|
void _onAssetUploaded() {
|
||||||
@@ -130,8 +134,38 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resumeBackup() {
|
||||||
|
debugPrint("[resumeBackup]");
|
||||||
|
var authState = ref.read(authenticationProvider);
|
||||||
|
|
||||||
|
// Check if user is login
|
||||||
|
var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
|
||||||
|
|
||||||
|
// User has been logged out return
|
||||||
|
if (accessKey == null || !authState.isAuthenticated) {
|
||||||
|
debugPrint("[resumeBackup] not authenticated - abort");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this device is enable backup by the user
|
||||||
|
if ((authState.deviceInfo.deviceId == authState.deviceId) && authState.deviceInfo.isAutoBackup) {
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("[resumeBackup] User disables auto backup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
||||||
return BackupNotifier();
|
return BackupNotifier(ref);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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/services.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
@@ -17,6 +18,7 @@ class VideoViewerPage extends StatelessWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -24,7 +26,7 @@ class VideoViewerPage extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
icon: const Icon(Icons.arrow_back_ios)),
|
icon: const Icon(Icons.arrow_back_ios)),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: SafeArea(
|
||||||
child: VideoThumbnailPlayer(
|
child: VideoThumbnailPlayer(
|
||||||
url: videoUrl,
|
url: videoUrl,
|
||||||
jwtToken: jwtToken,
|
jwtToken: jwtToken,
|
||||||
@@ -64,7 +66,6 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("ERROR initialize video player");
|
debugPrint("ERROR initialize video player");
|
||||||
print(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,22 @@
|
|||||||
FROM ubuntu:20.04 AS development
|
##################################
|
||||||
|
# DEVELOPMENT
|
||||||
|
##################################
|
||||||
|
FROM node:16-bullseye-slim AS development
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --fix-missing --no-install-recommends \
|
RUN apt-get update
|
||||||
build-essential \
|
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
||||||
curl \
|
|
||||||
git-core \
|
|
||||||
iputils-ping \
|
|
||||||
pkg-config \
|
|
||||||
rsync \
|
|
||||||
software-properties-common \
|
|
||||||
unzip \
|
|
||||||
wget \
|
|
||||||
ffmpeg
|
|
||||||
|
|
||||||
# Install NodeJS
|
RUN npm install
|
||||||
RUN curl --silent --location https://deb.nodesource.com/setup_14.x | bash -
|
|
||||||
RUN apt-get install --yes nodejs
|
|
||||||
|
|
||||||
RUN npm i -g yarn
|
|
||||||
|
|
||||||
RUN yarn install
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN yarn build
|
RUN npm run build
|
||||||
|
|
||||||
# Clean up commands
|
# Clean up commands
|
||||||
RUN apt-get autoremove -y && apt-get clean && \
|
RUN apt-get autoremove -y && apt-get clean && \
|
||||||
@@ -37,44 +25,37 @@ RUN apt-get autoremove -y && apt-get clean && \
|
|||||||
RUN apt-get clean && \
|
RUN apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
FROM ubuntu:20.04 as production
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
ARG NODE_ENV=production
|
|
||||||
ENV NODE_ENV=${NODE_ENV}
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
##################################
|
||||||
|
# PRODUCTION
|
||||||
|
##################################
|
||||||
|
# FROM node:16-bullseye-slim as production
|
||||||
|
# ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
# ARG NODE_ENV=production
|
||||||
|
# ENV NODE_ENV=${NODE_ENV}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --fix-missing --no-install-recommends \
|
# WORKDIR /usr/src/app
|
||||||
build-essential \
|
|
||||||
curl \
|
|
||||||
git-core \
|
|
||||||
iputils-ping \
|
|
||||||
pkg-config \
|
|
||||||
rsync \
|
|
||||||
software-properties-common \
|
|
||||||
unzip \
|
|
||||||
wget \
|
|
||||||
ffmpeg
|
|
||||||
|
|
||||||
# Install NodeJS
|
# COPY package.json yarn.lock ./
|
||||||
RUN curl --silent --location https://deb.nodesource.com/setup_14.x | bash -
|
|
||||||
RUN apt-get install --yes nodejs
|
|
||||||
|
|
||||||
RUN npm i -g yarn
|
# RUN apt-get update
|
||||||
|
# RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
||||||
|
|
||||||
RUN yarn install --only=production
|
# RUN npm i -g yarn --force
|
||||||
|
|
||||||
COPY . .
|
# RUN yarn install --only=production
|
||||||
|
|
||||||
COPY --from=development /usr/src/app/dist ./dist
|
# COPY . .
|
||||||
|
|
||||||
# Clean up commands
|
# COPY --from=development /usr/src/app/dist ./dist
|
||||||
RUN apt-get autoremove -y && apt-get clean && \
|
|
||||||
rm -rf /usr/local/src/*
|
|
||||||
|
|
||||||
RUN apt-get clean && \
|
# # Clean up commands
|
||||||
rm -rf /var/lib/apt/lists/*
|
# RUN apt-get autoremove -y && apt-get clean && \
|
||||||
|
# rm -rf /usr/local/src/*
|
||||||
|
|
||||||
CMD ["node", "dist/main"]
|
# RUN apt-get clean && \
|
||||||
|
# rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
# CMD ["node", "dist/main"]
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
##################################
|
|
||||||
# DEVELOPMENT
|
|
||||||
##################################
|
|
||||||
FROM node:16-bullseye-slim AS development
|
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
|
||||||
|
|
||||||
RUN apt-get update
|
|
||||||
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
|
||||||
|
|
||||||
RUN npm i -g yarn --force
|
|
||||||
|
|
||||||
RUN yarn install
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN yarn build
|
|
||||||
|
|
||||||
# Clean up commands
|
|
||||||
RUN apt-get autoremove -y && apt-get clean && \
|
|
||||||
rm -rf /usr/local/src/*
|
|
||||||
|
|
||||||
RUN apt-get clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##################################
|
|
||||||
# PRODUCTION
|
|
||||||
##################################
|
|
||||||
FROM node:16-bullseye-slim as production
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
ARG NODE_ENV=production
|
|
||||||
ENV NODE_ENV=${NODE_ENV}
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
|
||||||
|
|
||||||
RUN apt-get update
|
|
||||||
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
|
||||||
|
|
||||||
RUN npm i -g yarn --force
|
|
||||||
|
|
||||||
RUN yarn install --only=production
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
COPY --from=development /usr/src/app/dist ./dist
|
|
||||||
|
|
||||||
# Clean up commands
|
|
||||||
RUN apt-get autoremove -y && apt-get clean && \
|
|
||||||
rm -rf /usr/local/src/*
|
|
||||||
|
|
||||||
RUN apt-get clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
|
|
||||||
CMD ["node", "dist/main"]
|
|
||||||
@@ -1,13 +1 @@
|
|||||||
# IMMICH - Server
|
# Immich Server- NestJs
|
||||||
|
|
||||||
A self-hosted solution for mobile backup and viewing images/videos.
|
|
||||||
|
|
||||||
# Requesquisite
|
|
||||||
|
|
||||||
There is a tensorflow module running in the server so some package will be needed when building the Node's modules
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ apt-get install make cmake gcc g++
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
target: development
|
target: development
|
||||||
dockerfile: ./Dockerfile-minimal
|
dockerfile: ./Dockerfile
|
||||||
command: yarn start:dev
|
command: npm run start:dev
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
# expose:
|
# expose:
|
||||||
|
|||||||
18684
server/package-lock.json
generated
Normal file
18684
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,7 @@ import { Response as Res } from 'express';
|
|||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { stat } from 'fs';
|
import { stat } from 'fs';
|
||||||
import { pipeline } from 'stream';
|
import { pipeline } from 'stream';
|
||||||
|
import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
|
||||||
|
|
||||||
const fileInfo = promisify(stat);
|
const fileInfo = promisify(stat);
|
||||||
|
|
||||||
@@ -117,7 +118,6 @@ export class AssetController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Sending Partial Content With HTTP Code 206 */
|
/** Sending Partial Content With HTTP Code 206 */
|
||||||
console.log('Sendinf file with type ', asset.mimeType);
|
|
||||||
|
|
||||||
res.status(206).set({
|
res.status(206).set({
|
||||||
'Content-Range': `bytes ${start}-${end}/${size}`,
|
'Content-Range': `bytes ${start}-${end}/${size}`,
|
||||||
@@ -141,6 +141,11 @@ export class AssetController {
|
|||||||
console.log('SHOULD NOT BE HERE');
|
console.log('SHOULD NOT BE HERE');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/new')
|
||||||
|
async getNewAssets(@GetAuthUser() authUser: AuthUserDto, @Query(ValidationPipe) query: GetNewAssetQueryDto) {
|
||||||
|
return await this.assetService.getNewAssets(authUser, query.latestDate);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/all')
|
@Get('/all')
|
||||||
async getAllAssets(@GetAuthUser() authUser: AuthUserDto, @Query(ValidationPipe) query: GetAllAssetQueryDto) {
|
async getAllAssets(@GetAuthUser() authUser: AuthUserDto, @Query(ValidationPipe) query: GetAllAssetQueryDto) {
|
||||||
return await this.assetService.getAllAssets(authUser, query);
|
return await this.assetService.getAllAssets(authUser, query);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { MoreThan, Repository } from 'typeorm';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
import { UpdateAssetDto } from './dto/update-asset.dto';
|
import { UpdateAssetDto } from './dto/update-asset.dto';
|
||||||
@@ -8,6 +8,7 @@ import { AssetEntity, AssetType } from './entities/asset.entity';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
|
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
|
||||||
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
|
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
|
||||||
|
import { Greater } from '@tensorflow/tfjs-core';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssetService {
|
export class AssetService {
|
||||||
@@ -53,8 +54,6 @@ export class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getAllAssets(authUser: AuthUserDto, query: GetAllAssetQueryDto): Promise<GetAllAssetReponseDto> {
|
public async getAllAssets(authUser: AuthUserDto, query: GetAllAssetQueryDto): Promise<GetAllAssetReponseDto> {
|
||||||
// Each page will take 100 images.
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const assets = await this.assetRepository
|
const assets = await this.assetRepository
|
||||||
.createQueryBuilder('a')
|
.createQueryBuilder('a')
|
||||||
@@ -63,7 +62,7 @@ export class AssetService {
|
|||||||
lastQueryCreatedAt: query.nextPageKey || new Date().toISOString(),
|
lastQueryCreatedAt: query.nextPageKey || new Date().toISOString(),
|
||||||
})
|
})
|
||||||
.orderBy('a."createdAt"::date', 'DESC')
|
.orderBy('a."createdAt"::date', 'DESC')
|
||||||
// .take(500)
|
.take(5000)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
if (assets.length > 0) {
|
if (assets.length > 0) {
|
||||||
@@ -102,4 +101,16 @@ export class AssetService {
|
|||||||
|
|
||||||
return rows[0] as AssetEntity;
|
return rows[0] as AssetEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getNewAssets(authUser: AuthUserDto, latestDate: string) {
|
||||||
|
return await this.assetRepository.find({
|
||||||
|
where: {
|
||||||
|
userId: authUser.id,
|
||||||
|
createdAt: MoreThan(latestDate),
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
createdAt: 'ASC', // ASC order to add existed asset the latest group first before creating a new date group.
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
class GetAssetDto {
|
export class GetAssetDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
6
server/src/api-v1/asset/dto/get-new-asset-query.dto.ts
Normal file
6
server/src/api-v1/asset/dto/get-new-asset-query.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class GetNewAssetQueryDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
latestDate: string;
|
||||||
|
}
|
||||||
@@ -44,6 +44,6 @@ import { ServerInfoModule } from './api-v1/server-info/server-info.module';
|
|||||||
})
|
})
|
||||||
export class AppModule implements NestModule {
|
export class AppModule implements NestModule {
|
||||||
configure(consumer: MiddlewareConsumer): void {
|
configure(consumer: MiddlewareConsumer): void {
|
||||||
consumer.apply(AppLoggerMiddleware).forRoutes('*');
|
// consumer.apply(AppLoggerMiddleware).forRoutes('*');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export class ImageOptimizeProcessor {
|
|||||||
.toFile(resizePath, async (err, info) => {
|
.toFile(resizePath, async (err, info) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error resizing file ', err);
|
console.error('Error resizing file ', err);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.assetRepository.update(savedAsset, { resizePath: resizePath });
|
await this.assetRepository.update(savedAsset, { resizePath: resizePath });
|
||||||
@@ -66,7 +67,6 @@ export class ImageOptimizeProcessor {
|
|||||||
|
|
||||||
const basePath = this.configService.get('UPLOAD_LOCATION');
|
const basePath = this.configService.get('UPLOAD_LOCATION');
|
||||||
// const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
|
// const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
|
||||||
console.log(filename);
|
|
||||||
// Create folder for thumb image if not exist
|
// Create folder for thumb image if not exist
|
||||||
const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;
|
const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;
|
||||||
|
|
||||||
|
|||||||
6652
server/yarn.lock
6652
server/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user