Compare commits

...

24 Commits

Author SHA1 Message Date
Alex Tran
b04e69fd66 Update readme with screenshots 2022-02-08 14:18:51 -06:00
Alex Tran
0a1e28a08f Update readme 2022-02-08 14:06:58 -06:00
Alex Tran
9fdaa82d77 Update readme with gif 2022-02-08 14:05:31 -06:00
Alex Tran
88123b1cd2 Update readme with gif 2022-02-08 13:59:25 -06:00
Alex Tran
42c4c9dba1 Update readme with gif 2022-02-08 13:56:37 -06:00
Alex Tran
e63dc49475 Update readme with gif 2022-02-08 13:55:18 -06:00
Alex Tran
690f30f3dd Update Artifact path 2022-02-08 11:57:49 -06:00
Alex Tran
561b030e80 Update github action 2022-02-08 11:43:38 -06:00
Alex Tran
4756c075b6 Added work flow to build APK on push to master 2022-02-08 11:36:43 -06:00
Alex
328f382f86 Implemented multi select interaction (#13) 2022-02-08 11:24:49 -06:00
Alex Tran
6ad77e9434 Update readme 2022-02-07 23:55:30 -06:00
Alex
919928ab70 Implemented auto backup (#11) 2022-02-07 23:42:35 -06:00
Alex Tran
2a4d4ea999 Change docker hub name to the correct one 2022-02-07 16:20:21 -06:00
Alex Tran
547ce49500 Remove armv7-64bit for docker build as Tensorflow doesn't support that architecture, add amd64 2022-02-07 15:52:42 -06:00
Alex Tran
f4970ed053 Update readme 2022-02-07 15:44:03 -06:00
Alex Tran
9cf083decf Update readme 2022-02-07 15:25:51 -06:00
Alex Tran
d078367c04 change path in of target docker file in docker-compose for server 2022-02-07 15:11:59 -06:00
Alex Tran
a8edc85183 rename docker-minimal to dockerfile as target for github action 2022-02-07 15:06:30 -06:00
Alex Tran
5d48de7fa9 Change to npm instead of yarn in docker image to test for build error on github action 2022-02-07 14:58:23 -06:00
Alex Tran
82beb040bc Remove production build on docker file to test build for arm architecture 2022-02-07 14:38:02 -06:00
Alex Tran
03864e52ff Enable automated dockerhub image build 2022-02-07 08:55:15 -06:00
Alex
c24fb403c5 Implemented load new image when navigating back from backup page (#9) 2022-02-06 20:31:32 -06:00
Alex
1d3ee2008c Update workflow to build on pull request only 2022-02-06 13:11:17 -06:00
schklom
c917875943 Automated multi-platform build and DockerHub publication (#8)
* Automated multi-arch build

This setup uses GitHub Actions to build an image for arm/v7 and arm64 then publish them on DockerHub (you need to setup repo secrets first) every time you want (workflow_dispatch), every push, every pull requests (pull_requests), or on a schedule (cronjob) :)

Remove the triggers you don't want.

Reminder: if you ever move the Dockerfile (or some dependencies), you will have to correct the Dockerfile path (and/or the context path).

* Create dependabot.yml

This checks the dependencies' versions for the Actions everyday and creates a pull request if there are new versions available.
2022-02-06 13:06:01 -06:00
37 changed files with 19345 additions and 6960 deletions

7
.github/dependabot.yml vendored Normal file
View 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
View 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
View 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

View File

@@ -4,7 +4,21 @@
# IMMICH
Self-hosted Photo backup solution directly from your mobile phone.
Self-hosted photo and video backup solution directly from your mobile phone.
![](https://media.giphy.com/media/y8ZeaAigGmNvlSoKhU/giphy.gif)
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
@@ -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 !!**
# Features
[x] Upload assets(videos/images)
[x] View assets
[x] Quick navigation with drag scroll bar
[x] Auto Backup
# Development
You can use docker compose for development, there are several services that compose Immich
@@ -79,7 +103,7 @@ flutter run --release
# 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
more /proc/cpuinfo | grep flags

BIN
design/sc1.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

BIN
design/sc2.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

BIN
design/sc3.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 KiB

BIN
design/sc4.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

BIN
design/sc5.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 KiB

View File

@@ -1,16 +1 @@
# immich_mobile
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.
# Immich Mobile Application - Flutter

View File

@@ -4,6 +4,7 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/routing/router.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 'package:google_fonts/google_fonts.dart';
@@ -36,6 +37,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
case AppLifecycleState.resumed:
debugPrint("[APP STATE] resumed");
ref.read(appStateProvider.notifier).state = AppStateEnum.resumed;
ref.read(backupProvider.notifier).resumeBackup();
break;
case AppLifecycleState.inactive:
debugPrint("[APP STATE] inactive");
@@ -53,7 +55,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
}
Future<void> initApp() async {
// WidgetsBinding.instance?.addObserver(this);
WidgetsBinding.instance?.addObserver(this);
}
@override

View 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;
}

View File

@@ -1,15 +1,19 @@
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/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>> {
final imagePerPage = 100;
final AssetService _assetService = AssetService();
AssetNotifier() : super([]);
late String? nextPageKey = "";
bool isFetching = false;
// Get All assets
getImmichAssets() async {
GetAllAssetResponse? res = await _assetService.getAllAsset();
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) {
isFetching = true;
GetAllAssetResponse? res = await _assetService.getMoreAsset(nextPageKey);
GetAllAssetResponse? res = await _assetService.getOlderAsset(nextPageKey);
if (res != null) {
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() {
state = [];
}

View File

@@ -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()));

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.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';
class AssetService {
@@ -17,9 +18,10 @@ class AssetService {
} catch (e) {
debugPrint("Error getAllAsset ${e.toString()}");
}
return null;
}
Future<GetAllAssetResponse?> getMoreAsset(String? nextPageKey) async {
Future<GetAllAssetResponse?> getOlderAsset(String? nextPageKey) async {
try {
var res = await _networkService.getRequest(
url: "asset/all?nextPageKey=$nextPageKey",
@@ -34,5 +36,26 @@ class AssetService {
} catch (e) {
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 [];
}
}
}

View 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),
)
],
),
),
);
}
}

View File

@@ -2,7 +2,6 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.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/shared/models/backup_state.model.dart';
@@ -12,9 +11,11 @@ class ImmichSliverAppBar extends ConsumerWidget {
const ImmichSliverAppBar({
Key? key,
required this.imageGridGroup,
this.onPopBack,
}) : super(key: key);
final List<Widget> imageGridGroup;
final Function? onPopBack;
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -75,15 +76,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
if (onPop == true) {
// Remove and force getting new widget again if there is not many widget on screen.
// 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();
}
onPopBack!();
}
},
),

View 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,
),
),
),
);
}
}

View File

@@ -1,12 +1,9 @@
import 'package:auto_route/annotations.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart';
class ProfileDrawer extends ConsumerWidget {
const ProfileDrawer({Key? key}) : super(key: key);
@@ -58,10 +55,10 @@ class ProfileDrawer extends ConsumerWidget {
),
onTap: () async {
bool res = await ref.read(authenticationProvider.notifier).logout();
ref.read(assetProvider.notifier).clearAllAsset();
if (res) {
AutoRouter.of(context).popUntilRoot();
ref.read(assetProvider.notifier).clearAllAsset();
}
},
)

View File

@@ -1,66 +1,121 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.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/modules/home/providers/home_page_state.provider.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/routing/router.dart';
class ThumbnailImage extends HookWidget {
class ThumbnailImage extends HookConsumerWidget {
final ImmichAsset asset;
const ThumbnailImage({Key? key, required this.asset}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl =
'${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(
onTap: () {
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,
),
);
if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length == 1) {
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
} else if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length > 1) {
ref.watch(homePageStateProvider.notifier).removeSingleSelectedItem(asset);
} else if (isMultiSelectEnable && !selectedAsset.contains(asset)) {
ref.watch(homePageStateProvider.notifier).addSingleSelectedItem(asset);
} 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(
VideoViewerRoute(
videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
),
);
AutoRouter.of(context).push(
VideoViewerRoute(
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(
tag: asset.id,
child: CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
width: 300,
height: 300,
memCacheHeight: asset.type == 'IMAGE' ? 250 : 400,
fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
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);
},
child: Stack(
children: [
Container(
decoration: BoxDecoration(
border: isMultiSelectEnable && selectedAsset.contains(asset)
? Border.all(color: Theme.of(context).primaryColorLight, width: 10)
: const Border(),
),
child: CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
width: 300,
height: 300,
memCacheHeight: asset.type == 'IMAGE' ? 250 : 400,
fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
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(),
),
],
),
),
);

View File

@@ -1,36 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/image_grid.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/models/get_all_asset_respose.model.dart';
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
import 'package:intl/intl.dart';
class HomePage extends HookConsumerWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final ValueNotifier<bool> _showBackToTopBtn = useState(false);
ScrollController _scrollController = useScrollController();
List<ImmichAssetGroupByDate> assetGroup = ref.watch(assetProvider);
List<Widget> imageGridGroup = [];
List<ImmichAssetGroupByDate> _assetGroup = ref.watch(assetProvider);
List<Widget> _imageGridGroup = [];
_scrollControllerCallback() {
var endOfPage = _scrollController.position.maxScrollExtent;
if (_scrollController.offset >= endOfPage - (endOfPage * 0.1) && !_scrollController.position.outOfRange) {
ref.read(assetProvider.notifier).getMoreAsset();
}
if (_scrollController.offset >= 400) {
_showBackToTopBtn.value = true;
} else {
_showBackToTopBtn.value = false;
ref.read(assetProvider.notifier).getOlderAsset();
}
}
@@ -44,11 +37,23 @@ class HomePage extends HookConsumerWidget {
};
}, []);
Widget _buildBody() {
if (assetGroup.isNotEmpty) {
String lastGroupDate = assetGroup[0].date;
onPopBackFromBackupPage() {
ref.read(assetProvider.notifier).getNewAsset();
// 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 assetGroup = group.assets;
@@ -56,19 +61,22 @@ class HomePage extends HookConsumerWidget {
int? previousMonth = DateTime.tryParse(lastGroupDate)?.month;
// Add Monthly Title Group if started at the beginning of the month
if ((currentMonth! - previousMonth!) != 0) {
imageGridGroup.add(
MonthlyTitleText(isoDate: dateTitle),
);
if (currentMonth != null && previousMonth != null) {
if ((currentMonth - previousMonth) != 0) {
_imageGridGroup.add(
MonthlyTitleText(isoDate: dateTitle),
);
}
}
// Add Daily Title Group
imageGridGroup.add(
DailyTitleText(isoDate: dateTitle),
_imageGridGroup.add(
DailyTitleText(isoDate: dateTitle, assetGroup: assetGroup),
);
// Add Image Group
imageGridGroup.add(
_imageGridGroup.add(
ImageGrid(assetGroup: assetGroup),
);
//
@@ -84,8 +92,11 @@ class HomePage extends HookConsumerWidget {
child: CustomScrollView(
controller: _scrollController,
slivers: [
ImmichSliverAppBar(imageGridGroup: imageGridGroup),
...imageGridGroup,
ImmichSliverAppBar(
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,
),
),
),
],
),
),
);
}
}

View File

@@ -11,7 +11,7 @@ import 'package:immich_mobile/shared/services/network.service.dart';
import 'package:immich_mobile/shared/models/device_info.model.dart';
class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
AuthenticationNotifier()
AuthenticationNotifier(this.ref)
: super(
AuthenticationState(
deviceId: "",
@@ -31,6 +31,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
),
);
final Ref ref;
final DeviceInfoService _deviceInfoService = DeviceInfoService();
final BackupService _backupService = BackupService();
final NetworkService _networkService = NetworkService();
@@ -126,5 +127,5 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
}
final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
return AuthenticationNotifier();
return AuthenticationNotifier(ref);
});

View File

@@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:google_fonts/google_fonts.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/shared/providers/backup.provider.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class LoginForm extends HookConsumerWidget {
@@ -110,11 +112,16 @@ class LoginButton extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () async {
// This will remove current cache asset state of previous user login.
ref.watch(assetProvider.notifier).clearAllAsset();
var isAuthenicated = await ref
.read(authenticationProvider.notifier)
.login(emailController.text, passwordController.text, serverEndpointController.text);
if (isAuthenicated) {
// Resume backup (if enable) then navigate
ref.watch(backupProvider.notifier).resumeBackup();
AutoRouter.of(context).pushNamed("/home-page");
} else {
ImmichToast.show(

View File

@@ -1,6 +1,9 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.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/modules/login/providers/authentication.provider.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/server_info.model.dart';
@@ -8,7 +11,7 @@ import 'package:immich_mobile/shared/services/backup.service.dart';
import 'package:photo_manager/photo_manager.dart';
class BackupNotifier extends StateNotifier<BackUpState> {
BackupNotifier()
BackupNotifier(this.ref)
: super(
BackUpState(
backupProgress: BackUpProgressEnum.idle,
@@ -29,6 +32,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
),
);
final Ref ref;
final BackupService _backupService = BackupService();
final ServerInfoService _serverInfoService = ServerInfoService();
@@ -96,7 +100,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
void cancelBackup() {
state.cancelToken.cancel('Cancel Backup');
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
state = state.copyWith(backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
}
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) {
return BackupNotifier();
return BackupNotifier(ref);
});

View File

@@ -1,5 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:chewie/chewie.dart';
@@ -17,6 +18,7 @@ class VideoViewerPage extends StatelessWidget {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle.light,
backgroundColor: Colors.black,
leading: IconButton(
onPressed: () {
@@ -24,7 +26,7 @@ class VideoViewerPage extends StatelessWidget {
},
icon: const Icon(Icons.arrow_back_ios)),
),
body: Center(
body: SafeArea(
child: VideoThumbnailPlayer(
url: videoUrl,
jwtToken: jwtToken,
@@ -64,7 +66,6 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
setState(() {});
} catch (e) {
debugPrint("ERROR initialize video player");
print(e);
}
}

View File

@@ -1,34 +1,22 @@
FROM ubuntu:20.04 AS development
##################################
# DEVELOPMENT
##################################
FROM node:16-bullseye-slim AS development
ARG DEBIAN_FRONTEND=noninteractive
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 \
build-essential \
curl \
git-core \
iputils-ping \
pkg-config \
rsync \
software-properties-common \
unzip \
wget \
ffmpeg
RUN apt-get update
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
# Install NodeJS
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
RUN npm install
COPY . .
RUN yarn build
RUN npm run build
# Clean up commands
RUN apt-get autoremove -y && apt-get clean && \
@@ -37,44 +25,37 @@ RUN apt-get autoremove -y && apt-get clean && \
RUN apt-get clean && \
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 \
build-essential \
curl \
git-core \
iputils-ping \
pkg-config \
rsync \
software-properties-common \
unzip \
wget \
ffmpeg
# WORKDIR /usr/src/app
# Install NodeJS
RUN curl --silent --location https://deb.nodesource.com/setup_14.x | bash -
RUN apt-get install --yes nodejs
# COPY package.json yarn.lock ./
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
RUN apt-get autoremove -y && apt-get clean && \
rm -rf /usr/local/src/*
# COPY --from=development /usr/src/app/dist ./dist
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/*
# # Clean up commands
# 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"]

View File

@@ -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"]

View File

@@ -1,13 +1 @@
# IMMICH - Server
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++
```
# Immich Server- NestJs

View File

@@ -8,8 +8,8 @@ services:
build:
context: .
target: development
dockerfile: ./Dockerfile-minimal
command: yarn start:dev
dockerfile: ./Dockerfile
command: npm run start:dev
ports:
- "3000:3000"
# expose:

18684
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,7 @@ import { Response as Res } from 'express';
import { promisify } from 'util';
import { stat } from 'fs';
import { pipeline } from 'stream';
import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
const fileInfo = promisify(stat);
@@ -117,7 +118,6 @@ export class AssetController {
}
/** Sending Partial Content With HTTP Code 206 */
console.log('Sendinf file with type ', asset.mimeType);
res.status(206).set({
'Content-Range': `bytes ${start}-${end}/${size}`,
@@ -141,6 +141,11 @@ export class AssetController {
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')
async getAllAssets(@GetAuthUser() authUser: AuthUserDto, @Query(ValidationPipe) query: GetAllAssetQueryDto) {
return await this.assetService.getAllAssets(authUser, query);

View File

@@ -1,6 +1,6 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { MoreThan, Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateAssetDto } from './dto/create-asset.dto';
import { UpdateAssetDto } from './dto/update-asset.dto';
@@ -8,6 +8,7 @@ import { AssetEntity, AssetType } from './entities/asset.entity';
import _ from 'lodash';
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
import { Greater } from '@tensorflow/tfjs-core';
@Injectable()
export class AssetService {
@@ -53,8 +54,6 @@ export class AssetService {
}
public async getAllAssets(authUser: AuthUserDto, query: GetAllAssetQueryDto): Promise<GetAllAssetReponseDto> {
// Each page will take 100 images.
try {
const assets = await this.assetRepository
.createQueryBuilder('a')
@@ -63,7 +62,7 @@ export class AssetService {
lastQueryCreatedAt: query.nextPageKey || new Date().toISOString(),
})
.orderBy('a."createdAt"::date', 'DESC')
// .take(500)
.take(5000)
.getMany();
if (assets.length > 0) {
@@ -102,4 +101,16 @@ export class AssetService {
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.
},
});
}
}

View File

@@ -1,6 +1,6 @@
import { IsNotEmpty } from 'class-validator';
class GetAssetDto {
export class GetAssetDto {
@IsNotEmpty()
deviceId: string;
}

View File

@@ -0,0 +1,6 @@
import { IsNotEmpty } from 'class-validator';
export class GetNewAssetQueryDto {
@IsNotEmpty()
latestDate: string;
}

View File

@@ -44,6 +44,6 @@ import { ServerInfoModule } from './api-v1/server-info/server-info.module';
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer.apply(AppLoggerMiddleware).forRoutes('*');
// consumer.apply(AppLoggerMiddleware).forRoutes('*');
}
}

View File

@@ -42,6 +42,7 @@ export class ImageOptimizeProcessor {
.toFile(resizePath, async (err, info) => {
if (err) {
console.error('Error resizing file ', err);
return;
}
await this.assetRepository.update(savedAsset, { resizePath: resizePath });
@@ -66,7 +67,6 @@ export class ImageOptimizeProcessor {
const basePath = this.configService.get('UPLOAD_LOCATION');
// const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
console.log(filename);
// Create folder for thumb image if not exist
const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;

File diff suppressed because it is too large Load Diff