Переглянути джерело

Add video thumbnail with duration and icon

Alex Tran 3 роки тому
батько
коміт
69ed287974

+ 74 - 53
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<ImmichAsset> 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<VideoThumbnailPlayer> createState() => _VideoThumbnailPlayerState();
-}
+//   @override
+//   State<VideoThumbnailPlayer> createState() => _VideoThumbnailPlayerState();
+// }
 
-class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
-  late VideoPlayerController videoPlayerController;
-  ChewieController? chewieController;
+// class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
+//   late VideoPlayerController videoPlayerController;
+//   ChewieController? chewieController;
 
-  @override
-  void initState() {
-    super.initState();
-    initializePlayer();
-  }
+//   @override
+//   void initState() {
+//     super.initState();
+//     initializePlayer();
+//   }
 
-  Future<void> initializePlayer() async {
-    videoPlayerController =
-        VideoPlayerController.network('https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4');
+//   Future<void> 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");
+//   }
+// }

+ 6 - 1
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);
           },
         ),

+ 3 - 24
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<bool> _showBackToTopBtn = useState(false);
     ScrollController _scrollController = useScrollController();
+
     List<ImmichAssetGroupByDate> assetGroup = ref.watch(assetProvider);
     List<Widget> 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,

+ 9 - 9
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;
   }
 }

+ 2 - 3
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,

+ 1 - 1
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);
       }
     });
 

+ 3 - 3
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) {

+ 1 - 4
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;
 }

+ 2 - 8
server/src/api-v1/asset/entities/asset.entity.ts

@@ -34,16 +34,10 @@ export class AssetEntity {
   isFavorite: boolean;
 
   @Column({ nullable: true })
-  description: string;
-
-  @Column({ nullable: true })
-  lat: string;
-
-  @Column({ nullable: true })
-  lon: string;
+  mimeType: string;
 
   @Column({ nullable: true })
-  mimeType: string;
+  duration: string;
 }
 
 export enum AssetType {

+ 13 - 15
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');
-      })
-      .on('end', async () => {
-        await this.assetRepository.update(savedAsset, { resizePath: resizePath });
+      .thumbnail({
+        count: 1,
+        timestamps: [1],
+        folder: resizeDir,
+        filename: `${filename}.png`,
+        size: '512x512',
       })
-      .run();
+      .on('end', async (a) => {
+        await this.assetRepository.update(savedAsset, { resizePath: `${resizeDir}/${filename}.png` });
+      });
 
     return 'ok';
   }

+ 3 - 4
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() },
     );