diff --git a/mobile/lib/modules/home/ui/image_grid.dart b/mobile/lib/modules/home/ui/image_grid.dart index 470520aa30..18238d8779 100644 --- a/mobile/lib/modules/home/ui/image_grid.dart +++ b/mobile/lib/modules/home/ui/image_grid.dart @@ -1,16 +1,15 @@ -import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; import 'package:immich_mobile/shared/models/immich_asset.model.dart'; -import 'package:video_player/video_player.dart'; -class ImageGrid extends StatelessWidget { +class ImageGrid extends ConsumerWidget { final List assetGroup; const ImageGrid({Key? key, required this.assetGroup}) : super(key: key); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 5.0, mainAxisSpacing: 5), @@ -19,11 +18,33 @@ class ImageGrid extends StatelessWidget { var assetType = assetGroup[index].type; return GestureDetector( - onTap: () {}, - child: assetType == 'IMAGE' - ? ThumbnailImage(asset: assetGroup[index]) - : VideoThumbnailPlayer(key: Key(assetGroup[index].id), videoAsset: assetGroup[index]), - ); + onTap: () {}, + child: Stack( + children: [ + ThumbnailImage(asset: assetGroup[index]), + assetType == 'IMAGE' + ? Container() + : Positioned( + top: 5, + right: 5, + child: Row( + children: [ + Text( + assetGroup[index].duration.toString().substring(0, 7), + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + const Icon( + Icons.play_circle_outline_rounded, + color: Colors.white, + ), + ], + ), + ) + ], + )); }, childCount: assetGroup.length, ), @@ -31,55 +52,55 @@ class ImageGrid extends StatelessWidget { } } -class VideoThumbnailPlayer extends StatefulWidget { - ImmichAsset videoAsset; +// class VideoThumbnailPlayer extends StatefulWidget { +// ImmichAsset videoAsset; - VideoThumbnailPlayer({Key? key, required this.videoAsset}) : super(key: key); +// VideoThumbnailPlayer({Key? key, required this.videoAsset}) : super(key: key); - @override - State createState() => _VideoThumbnailPlayerState(); -} +// @override +// State createState() => _VideoThumbnailPlayerState(); +// } -class _VideoThumbnailPlayerState extends State { - late VideoPlayerController videoPlayerController; - ChewieController? chewieController; +// class _VideoThumbnailPlayerState extends State { +// late VideoPlayerController videoPlayerController; +// ChewieController? chewieController; - @override - void initState() { - super.initState(); - initializePlayer(); - } +// @override +// void initState() { +// super.initState(); +// initializePlayer(); +// } - Future initializePlayer() async { - videoPlayerController = - VideoPlayerController.network('https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4'); +// Future initializePlayer() async { +// videoPlayerController = +// VideoPlayerController.network('https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4'); - await Future.wait([ - videoPlayerController.initialize(), - ]); - _createChewieController(); - setState(() {}); - } +// await Future.wait([ +// videoPlayerController.initialize(), +// ]); +// _createChewieController(); +// setState(() {}); +// } - _createChewieController() { - chewieController = ChewieController( - showControlsOnInitialize: false, - videoPlayerController: videoPlayerController, - autoPlay: true, - looping: true, - ); - } +// _createChewieController() { +// chewieController = ChewieController( +// showControlsOnInitialize: false, +// videoPlayerController: videoPlayerController, +// autoPlay: true, +// looping: true, +// ); +// } - @override - Widget build(BuildContext context) { - return chewieController != null && chewieController!.videoPlayerController.value.isInitialized - ? SizedBox( - height: 300, - width: 300, - child: Chewie( - controller: chewieController!, - ), - ) - : const Text("Loading Video"); - } -} +// @override +// Widget build(BuildContext context) { +// return chewieController != null && chewieController!.videoPlayerController.value.isInitialized +// ? SizedBox( +// height: 300, +// width: 300, +// child: Chewie( +// controller: chewieController!, +// ), +// ) +// : const Text("Loading Video"); +// } +// } diff --git a/mobile/lib/modules/home/ui/thumbnail_image.dart b/mobile/lib/modules/home/ui/thumbnail_image.dart index ca8ae1d6a7..1225fd888d 100644 --- a/mobile/lib/modules/home/ui/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/thumbnail_image.dart @@ -1,18 +1,21 @@ import 'package:auto_route/auto_route.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/shared/models/immich_asset.model.dart'; import 'package:immich_mobile/routing/router.dart'; -class ThumbnailImage extends StatelessWidget { +class ThumbnailImage extends HookWidget { final ImmichAsset asset; const ThumbnailImage({Key? key, required this.asset}) : super(key: key); @override Widget build(BuildContext context) { + final cacheKey = useState(1); + var box = Hive.box(userInfoBox); var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true'; @@ -31,6 +34,7 @@ class ThumbnailImage extends StatelessWidget { child: Hero( tag: asset.id, child: CachedNetworkImage( + cacheKey: "${asset.id}-${cacheKey.value}", width: 300, height: 300, memCacheHeight: 250, @@ -44,6 +48,7 @@ class ThumbnailImage extends StatelessWidget { ), errorWidget: (context, url, error) { debugPrint("Error Loading Thumbnail Widget $error"); + cacheKey.value += 1; return const Icon(Icons.error); }, ), diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 90c6b0c3b4..7978d4c732 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -2,10 +2,10 @@ 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/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/profile_drawer.dart'; import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart'; -import 'package:immich_mobile/modules/home/ui/image_grid.dart'; import 'package:immich_mobile/modules/home/providers/asset.provider.dart'; import 'package:intl/intl.dart'; @@ -16,9 +16,9 @@ class HomePage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ValueNotifier _showBackToTopBtn = useState(false); ScrollController _scrollController = useScrollController(); + List assetGroup = ref.watch(assetProvider); List imageGridGroup = []; - String scrollBarText = ""; _scrollControllerCallback() { var endOfPage = _scrollController.position.maxScrollExtent; @@ -40,7 +40,6 @@ class HomePage extends HookConsumerWidget { _scrollController.addListener(_scrollControllerCallback); return () { - debugPrint("Remove scroll listener"); _scrollController.removeListener(_scrollControllerCallback); }; }, []); @@ -72,33 +71,13 @@ class HomePage extends HookConsumerWidget { imageGridGroup.add( ImageGrid(assetGroup: assetGroup), ); - + // lastGroupDate = dateTitle; } } return SafeArea( child: DraggableScrollbar.semicircle( - // labelTextBuilder: (offset) { - // final int currentItem = _scrollController.hasClients - // ? (_scrollController.offset / _scrollController.position.maxScrollExtent * imageGridGroup.length) - // .floor() - // : 0; - - // if (imageGridGroup[currentItem] is DailyTitleText) { - // DailyTitleText item = imageGridGroup[currentItem] as DailyTitleText; - // debugPrint(item.isoDate); - // return const Text(""); - // } - - // if (imageGridGroup[currentItem] is MonthlyTitleText) { - // MonthlyTitleText item = imageGridGroup[currentItem] as MonthlyTitleText; - // debugPrint(item.isoDate); - // return const Text("scrollBarText"); - // } - // return const Text("scrollBarText"); - // }, - // labelConstraints: const BoxConstraints.tightFor(width: 200.0, height: 30.0), backgroundColor: Theme.of(context).primaryColor, controller: _scrollController, heightScrollThumb: 48.0, diff --git a/mobile/lib/shared/models/immich_asset.model.dart b/mobile/lib/shared/models/immich_asset.model.dart index 8dad845c24..0073dfccc7 100644 --- a/mobile/lib/shared/models/immich_asset.model.dart +++ b/mobile/lib/shared/models/immich_asset.model.dart @@ -9,7 +9,7 @@ class ImmichAsset { final String createdAt; final String modifiedAt; final bool isFavorite; - final String? description; + final String? duration; ImmichAsset({ required this.id, @@ -20,7 +20,7 @@ class ImmichAsset { required this.createdAt, required this.modifiedAt, required this.isFavorite, - this.description, + this.duration, }); ImmichAsset copyWith({ @@ -32,7 +32,7 @@ class ImmichAsset { String? createdAt, String? modifiedAt, bool? isFavorite, - String? description, + String? duration, }) { return ImmichAsset( id: id ?? this.id, @@ -43,7 +43,7 @@ class ImmichAsset { createdAt: createdAt ?? this.createdAt, modifiedAt: modifiedAt ?? this.modifiedAt, isFavorite: isFavorite ?? this.isFavorite, - description: description ?? this.description, + duration: duration ?? this.duration, ); } @@ -57,7 +57,7 @@ class ImmichAsset { 'createdAt': createdAt, 'modifiedAt': modifiedAt, 'isFavorite': isFavorite, - 'description': description, + 'duration': duration, }; } @@ -71,7 +71,7 @@ class ImmichAsset { createdAt: map['createdAt'] ?? '', modifiedAt: map['modifiedAt'] ?? '', isFavorite: map['isFavorite'] ?? false, - description: map['description'], + duration: map['duration'], ); } @@ -81,7 +81,7 @@ class ImmichAsset { @override String toString() { - return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, description: $description)'; + return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, duration: $duration)'; } @override @@ -97,7 +97,7 @@ class ImmichAsset { other.createdAt == createdAt && other.modifiedAt == modifiedAt && other.isFavorite == isFavorite && - other.description == description; + other.duration == duration; } @override @@ -110,6 +110,6 @@ class ImmichAsset { createdAt.hashCode ^ modifiedAt.hashCode ^ isFavorite.hashCode ^ - description.hashCode; + duration.hashCode; } } diff --git a/mobile/lib/shared/services/backup.service.dart b/mobile/lib/shared/services/backup.service.dart index 5ecdd4d103..3fb1975712 100644 --- a/mobile/lib/shared/services/backup.service.dart +++ b/mobile/lib/shared/services/backup.service.dart @@ -49,8 +49,8 @@ class BackupService { String originalFileName = await entity.titleAsync; String fileNameWithoutPath = originalFileName.toString().split(".")[0]; var fileExtension = p.extension(file.path); - LatLng coordinate = await entity.latlngAsync(); var mimeType = FileHelper.getMimeType(file.path); + var formData = FormData.fromMap({ 'deviceAssetId': entity.id, 'deviceId': deviceId, @@ -59,8 +59,7 @@ class BackupService { 'modifiedAt': entity.modifiedDateTime.toIso8601String(), 'isFavorite': entity.isFavorite, 'fileExtension': fileExtension, - 'lat': coordinate.latitude, - 'lon': coordinate.longitude, + 'duration': entity.videoDuration, 'files': [ await MultipartFile.fromFile( file.path, diff --git a/server/src/api-v1/asset/asset.controller.ts b/server/src/api-v1/asset/asset.controller.ts index 6df61a422b..116f4fb3e3 100644 --- a/server/src/api-v1/asset/asset.controller.ts +++ b/server/src/api-v1/asset/asset.controller.ts @@ -49,7 +49,7 @@ export class AssetController { } if (savedAsset && savedAsset.type == AssetType.VIDEO) { - await this.assetOptimizeService.resizeVideo(savedAsset); + await this.assetOptimizeService.getVideoThumbnail(savedAsset, file.originalname); } }); diff --git a/server/src/api-v1/asset/asset.service.ts b/server/src/api-v1/asset/asset.service.ts index 8c8532c3c9..47f41bee99 100644 --- a/server/src/api-v1/asset/asset.service.ts +++ b/server/src/api-v1/asset/asset.service.ts @@ -26,9 +26,9 @@ export class AssetService { asset.createdAt = assetInfo.createdAt; asset.modifiedAt = assetInfo.modifiedAt; asset.isFavorite = assetInfo.isFavorite; - asset.lat = assetInfo.lat; - asset.lon = assetInfo.lon; asset.mimeType = mimeType; + asset.duration = assetInfo.duration; + try { const res = await this.assetRepository.save(asset); @@ -63,7 +63,7 @@ export class AssetService { lastQueryCreatedAt: query.nextPageKey || new Date().toISOString(), }) .orderBy('a."createdAt"::date', 'DESC') - .take(10000) + // .take(5000) .getMany(); if (assets.length > 0) { diff --git a/server/src/api-v1/asset/dto/create-asset.dto.ts b/server/src/api-v1/asset/dto/create-asset.dto.ts index ce95421783..f3e469ed00 100644 --- a/server/src/api-v1/asset/dto/create-asset.dto.ts +++ b/server/src/api-v1/asset/dto/create-asset.dto.ts @@ -24,8 +24,5 @@ export class CreateAssetDto { fileExtension: string; @IsOptional() - lat: string; - - @IsOptional() - lon: string; + duration: string; } diff --git a/server/src/api-v1/asset/entities/asset.entity.ts b/server/src/api-v1/asset/entities/asset.entity.ts index 97343004fa..4d78096d25 100644 --- a/server/src/api-v1/asset/entities/asset.entity.ts +++ b/server/src/api-v1/asset/entities/asset.entity.ts @@ -33,17 +33,11 @@ export class AssetEntity { @Column({ type: 'boolean', default: false }) isFavorite: boolean; - @Column({ nullable: true }) - description: string; - - @Column({ nullable: true }) - lat: string; - - @Column({ nullable: true }) - lon: string; - @Column({ nullable: true }) mimeType: string; + + @Column({ nullable: true }) + duration: string; } export enum AssetType { diff --git a/server/src/modules/image-optimize/image-optimize.processor.ts b/server/src/modules/image-optimize/image-optimize.processor.ts index 026cb68840..f34c812c84 100644 --- a/server/src/modules/image-optimize/image-optimize.processor.ts +++ b/server/src/modules/image-optimize/image-optimize.processor.ts @@ -60,13 +60,13 @@ export class ImageOptimizeProcessor { return 'ok'; } - @Process('resize-video') + @Process('get-video-thumbnail') async resizeUploadedVideo(job: Job) { - const { savedAsset }: { savedAsset: AssetEntity } = job.data; + const { savedAsset, filename }: { savedAsset: AssetEntity; filename: String } = job.data; 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 const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`; @@ -75,18 +75,16 @@ export class ImageOptimizeProcessor { } ffmpeg(savedAsset.originalPath) - .output(resizePath) - .noAudio() - .videoCodec('libx264') - .size('640x?') - .aspect('4:3') - .on('error', (e) => { - Logger.log(`Error resizing File: ${e}`, 'resizeUploadedVideo'); + .thumbnail({ + count: 1, + timestamps: [1], + folder: resizeDir, + filename: `${filename}.png`, + size: '512x512', }) - .on('end', async () => { - await this.assetRepository.update(savedAsset, { resizePath: resizePath }); - }) - .run(); + .on('end', async (a) => { + await this.assetRepository.update(savedAsset, { resizePath: `${resizeDir}/${filename}.png` }); + }); return 'ok'; } diff --git a/server/src/modules/image-optimize/image-optimize.service.ts b/server/src/modules/image-optimize/image-optimize.service.ts index 4824e9ffd6..37f7a488be 100644 --- a/server/src/modules/image-optimize/image-optimize.service.ts +++ b/server/src/modules/image-optimize/image-optimize.service.ts @@ -2,9 +2,7 @@ import { InjectQueue } from '@nestjs/bull'; import { Injectable } from '@nestjs/common'; import { Queue } from 'bull'; import { randomUUID } from 'crypto'; -import { join } from 'path'; import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; -import { AuthUserDto } from '../../decorators/auth-user.decorator'; @Injectable() export class AssetOptimizeService { @@ -24,11 +22,12 @@ export class AssetOptimizeService { }; } - public async resizeVideo(savedAsset: AssetEntity) { + public async getVideoThumbnail(savedAsset: AssetEntity, filename: String) { const job = await this.optimizeQueue.add( - 'resize-video', + 'get-video-thumbnail', { savedAsset, + filename, }, { jobId: randomUUID() }, );