From c24fb403c567e2463672ad10ebe82ed1284e76ef Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 6 Feb 2022 20:31:32 -0600 Subject: [PATCH] Implemented load new image when navigating back from backup page (#9) --- .github/workflows/Build+push Immich.yml | 14 +++--- README.md | 2 +- .../home/providers/asset.provider.dart | 45 +++++++++++++++++-- .../modules/home/services/asset.service.dart | 25 ++++++++++- .../modules/home/ui/immich_sliver_appbar.dart | 13 ++---- .../lib/modules/home/ui/profile_drawer.dart | 4 +- mobile/lib/modules/home/views/home_page.dart | 30 ++++++++++--- .../lib/shared/views/video_viewer_page.dart | 5 ++- server/src/api-v1/asset/asset.controller.ts | 6 +++ server/src/api-v1/asset/asset.service.ts | 19 ++++++-- server/src/api-v1/asset/dto/get-asset.dto.ts | 2 +- .../asset/dto/get-new-asset-query.dto.ts | 6 +++ 12 files changed, 133 insertions(+), 38 deletions(-) create mode 100644 server/src/api-v1/asset/dto/get-new-asset-query.dto.ts diff --git a/.github/workflows/Build+push Immich.yml b/.github/workflows/Build+push Immich.yml index b633f696876..271911c1a1d 100644 --- a/.github/workflows/Build+push Immich.yml +++ b/.github/workflows/Build+push Immich.yml @@ -3,13 +3,13 @@ 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 * * *' + # * 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 ] + # push: + #branches: [ main ] + # pull_request: + # branches: [ main ] jobs: buildandpush: @@ -18,7 +18,7 @@ jobs: - name: Checkout uses: actions/checkout@v2.4.0 with: - ref: 'main' # branch + ref: "main" # branch # https://github.com/docker/setup-qemu-action#usage - name: Set up QEMU uses: docker/setup-qemu-action@v1.2.0 diff --git a/README.md b/README.md index 3f6e6ad63f2..9313ff94c08 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,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 diff --git a/mobile/lib/modules/home/providers/asset.provider.dart b/mobile/lib/modules/home/providers/asset.provider.dart index 6eca6f13266..92ec188a258 100644 --- a/mobile/lib/modules/home/providers/asset.provider.dart +++ b/mobile/lib/modules/home/providers/asset.provider.dart @@ -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> { - 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> { } } - 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> { } } + // 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((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 newAssets = await _assetService.getNewAsset(latestAsset.createdAt); + + if (newAssets.isEmpty) { + return; + } + + // Grouping by data + var groupByDateList = groupBy( + 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 = []; } diff --git a/mobile/lib/modules/home/services/asset.service.dart b/mobile/lib/modules/home/services/asset.service.dart index 63a99b40282..51d15285284 100644 --- a/mobile/lib/modules/home/services/asset.service.dart +++ b/mobile/lib/modules/home/services/asset.service.dart @@ -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 getMoreAsset(String? nextPageKey) async { + Future 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> getNewAsset(String latestDate) async { + try { + var res = await _networkService.getRequest( + url: "asset/new?latestDate=$latestDate", + ); + + List decodedData = jsonDecode(res.toString()); + + List result = List.from(decodedData.map((a) => ImmichAsset.fromMap(a))); + if (result.isNotEmpty) { + return result; + } + + return []; + } catch (e) { + debugPrint("Error getAllAsset ${e.toString()}"); + return []; + } } } diff --git a/mobile/lib/modules/home/ui/immich_sliver_appbar.dart b/mobile/lib/modules/home/ui/immich_sliver_appbar.dart index 055403034b6..b1f0013cfb2 100644 --- a/mobile/lib/modules/home/ui/immich_sliver_appbar.dart +++ b/mobile/lib/modules/home/ui/immich_sliver_appbar.dart @@ -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 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!(); } }, ), diff --git a/mobile/lib/modules/home/ui/profile_drawer.dart b/mobile/lib/modules/home/ui/profile_drawer.dart index 03b96d2d7fd..0114c17fe34 100644 --- a/mobile/lib/modules/home/ui/profile_drawer.dart +++ b/mobile/lib/modules/home/ui/profile_drawer.dart @@ -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,6 +55,7 @@ class ProfileDrawer extends ConsumerWidget { ), onTap: () async { bool res = await ref.read(authenticationProvider.notifier).logout(); + ref.read(assetProvider.notifier).clearAllAsset(); if (res) { diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 7978d4c7321..8e277486ddc 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -24,7 +24,7 @@ class HomePage extends HookConsumerWidget { var endOfPage = _scrollController.position.maxScrollExtent; if (_scrollController.offset >= endOfPage - (endOfPage * 0.1) && !_scrollController.position.outOfRange) { - ref.read(assetProvider.notifier).getMoreAsset(); + ref.read(assetProvider.notifier).getOlderAsset(); } if (_scrollController.offset >= 400) { @@ -44,6 +44,18 @@ class HomePage extends HookConsumerWidget { }; }, []); + onPopBackFromBackupPage() { + ref.read(assetProvider.notifier).getNewAsset(); + // Remove and force getting new widget again if there is not many widget on screen. + // Otherwise do nothing. + + 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; @@ -56,10 +68,13 @@ 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 @@ -84,7 +99,10 @@ class HomePage extends HookConsumerWidget { child: CustomScrollView( controller: _scrollController, slivers: [ - ImmichSliverAppBar(imageGridGroup: imageGridGroup), + ImmichSliverAppBar( + imageGridGroup: imageGridGroup, + onPopBack: onPopBackFromBackupPage, + ), ...imageGridGroup, ], ), diff --git a/mobile/lib/shared/views/video_viewer_page.dart b/mobile/lib/shared/views/video_viewer_page.dart index 9f42a522043..c80393d233c 100644 --- a/mobile/lib/shared/views/video_viewer_page.dart +++ b/mobile/lib/shared/views/video_viewer_page.dart @@ -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 { setState(() {}); } catch (e) { debugPrint("ERROR initialize video player"); - print(e); } } diff --git a/server/src/api-v1/asset/asset.controller.ts b/server/src/api-v1/asset/asset.controller.ts index acc1e5a0523..dd756689a71 100644 --- a/server/src/api-v1/asset/asset.controller.ts +++ b/server/src/api-v1/asset/asset.controller.ts @@ -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); @@ -141,6 +142,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); diff --git a/server/src/api-v1/asset/asset.service.ts b/server/src/api-v1/asset/asset.service.ts index aea2970ba2f..8196986efd7 100644 --- a/server/src/api-v1/asset/asset.service.ts +++ b/server/src/api-v1/asset/asset.service.ts @@ -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 { - // 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. + }, + }); + } } diff --git a/server/src/api-v1/asset/dto/get-asset.dto.ts b/server/src/api-v1/asset/dto/get-asset.dto.ts index c11d46787f9..fa598e2fa8a 100644 --- a/server/src/api-v1/asset/dto/get-asset.dto.ts +++ b/server/src/api-v1/asset/dto/get-asset.dto.ts @@ -1,6 +1,6 @@ import { IsNotEmpty } from 'class-validator'; -class GetAssetDto { +export class GetAssetDto { @IsNotEmpty() deviceId: string; } diff --git a/server/src/api-v1/asset/dto/get-new-asset-query.dto.ts b/server/src/api-v1/asset/dto/get-new-asset-query.dto.ts new file mode 100644 index 00000000000..f10b3bec801 --- /dev/null +++ b/server/src/api-v1/asset/dto/get-new-asset-query.dto.ts @@ -0,0 +1,6 @@ +import { IsNotEmpty } from 'class-validator'; + +export class GetNewAssetQueryDto { + @IsNotEmpty() + latestDate: string; +}