mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
Implemented Video Upload and Player (#2)
* Implementing video upload features * setup image resize processor * Add video thumbnail with duration and icon * Fixed issue with video upload timeout and upper case file type on ios * Added video player page * Added video player page * Fixing video player not play on ios * Added partial file streaming for ios/android video request * Added nginx as proxy server for better file serving * update nginx and docker-compose file * Video player working correctly * Video player working correctly * Split duration to the second
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
@@ -12,6 +13,12 @@ void main() async {
|
||||
// Hive.registerAdapter(ImmichBackUpAssetAdapter());
|
||||
// Hive.deleteBoxFromDisk(hiveImmichBox);
|
||||
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
),
|
||||
);
|
||||
|
||||
runApp(const ProviderScope(child: ImmichApp()));
|
||||
}
|
||||
|
||||
@@ -69,6 +76,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
||||
title: 'Immich',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
brightness: Brightness.light,
|
||||
primarySwatch: Colors.indigo,
|
||||
textTheme: GoogleFonts.workSansTextTheme(
|
||||
Theme.of(context).textTheme.apply(fontSizeFactor: 1.0),
|
||||
@@ -79,6 +87,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
||||
foregroundColor: Colors.indigo,
|
||||
elevation: 1,
|
||||
centerTitle: true,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||
),
|
||||
),
|
||||
routeInformationParser: _immichRouter.defaultRouteParser(),
|
||||
|
||||
@@ -1,23 +1,50 @@
|
||||
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';
|
||||
|
||||
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),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
var assetType = assetGroup[index].type;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {},
|
||||
child: ThumbnailImage(asset: 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,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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';
|
||||
@@ -100,14 +99,6 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
],
|
||||
systemOverlayStyle: const SystemUiOverlayStyle(
|
||||
// Status bar color
|
||||
statusBarColor: Colors.indigo,
|
||||
|
||||
// Status bar brightness (optional)
|
||||
statusBarIconBrightness: Brightness.light, // For Android (dark icons)
|
||||
statusBarBrightness: Brightness.dark,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,39 +1,53 @@
|
||||
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';
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
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 (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}',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () {},
|
||||
child: Hero(
|
||||
tag: asset.id,
|
||||
child: CachedNetworkImage(
|
||||
cacheKey: "${asset.id}-${cacheKey.value}",
|
||||
width: 300,
|
||||
height: 300,
|
||||
memCacheHeight: 250,
|
||||
memCacheHeight: asset.type == 'IMAGE' ? 250 : 400,
|
||||
fit: BoxFit.cover,
|
||||
imageUrl: thumbnailRequestUrl,
|
||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||
@@ -44,6 +58,7 @@ class ThumbnailImage extends StatelessWidget {
|
||||
),
|
||||
errorWidget: (context, url, error) {
|
||||
debugPrint("Error Loading Thumbnail Widget $error");
|
||||
cacheKey.value += 1;
|
||||
return const Icon(Icons.error);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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 = [];
|
||||
final scrollLabelText = useState("");
|
||||
|
||||
_scrollControllerCallback() {
|
||||
var endOfPage = _scrollController.position.maxScrollExtent;
|
||||
@@ -40,39 +40,10 @@ class HomePage extends HookConsumerWidget {
|
||||
_scrollController.addListener(_scrollControllerCallback);
|
||||
|
||||
return () {
|
||||
debugPrint("Remove scroll listener");
|
||||
_scrollController.removeListener(_scrollControllerCallback);
|
||||
};
|
||||
}, []);
|
||||
|
||||
SliverToBoxAdapter _buildDateGroupTitle(String dateTitle) {
|
||||
var currentYear = DateTime.now().year;
|
||||
var groupYear = DateTime.parse(dateTitle).year;
|
||||
var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
|
||||
var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(dateTitle));
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (assetGroup.isNotEmpty) {
|
||||
String lastGroupDate = assetGroup[0].date;
|
||||
@@ -86,44 +57,27 @@ class HomePage extends HookConsumerWidget {
|
||||
|
||||
// Add Monthly Title Group if started at the beginning of the month
|
||||
if ((currentMonth! - previousMonth!) != 0) {
|
||||
var monthTitleText = DateFormat('MMMM, y').format(DateTime.parse(dateTitle));
|
||||
|
||||
imageGridGroup.add(
|
||||
MonthlyTitleText(monthTitleText: monthTitleText),
|
||||
MonthlyTitleText(isoDate: dateTitle),
|
||||
);
|
||||
}
|
||||
|
||||
// Add Daily Title Group
|
||||
imageGridGroup.add(
|
||||
DailyTitleText(dateTitle: dateTitle),
|
||||
DailyTitleText(isoDate: dateTitle),
|
||||
);
|
||||
|
||||
// Add Image Group
|
||||
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 MonthlyTitleText) {
|
||||
// MonthlyTitleText item = imageGridGroup[currentItem] as MonthlyTitleText;
|
||||
|
||||
// scrollLabelText.value = item.monthTitleText;
|
||||
// }
|
||||
|
||||
// return Text(scrollLabelText.value);
|
||||
// },
|
||||
// labelConstraints: const BoxConstraints.tightFor(width: 200.0, height: 30.0),
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
controller: _scrollController,
|
||||
heightScrollThumb: 48.0,
|
||||
@@ -148,13 +102,15 @@ class HomePage extends HookConsumerWidget {
|
||||
class MonthlyTitleText extends StatelessWidget {
|
||||
const MonthlyTitleText({
|
||||
Key? key,
|
||||
required this.monthTitleText,
|
||||
required this.isoDate,
|
||||
}) : super(key: key);
|
||||
|
||||
final String monthTitleText;
|
||||
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),
|
||||
@@ -174,17 +130,17 @@ class MonthlyTitleText extends StatelessWidget {
|
||||
class DailyTitleText extends StatelessWidget {
|
||||
const DailyTitleText({
|
||||
Key? key,
|
||||
required this.dateTitle,
|
||||
required this.isoDate,
|
||||
}) : super(key: key);
|
||||
|
||||
final String dateTitle;
|
||||
final String isoDate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var currentYear = DateTime.now().year;
|
||||
var groupYear = DateTime.parse(dateTitle).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(dateTitle));
|
||||
var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
|
||||
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
|
||||
@@ -13,7 +13,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final usernameController = useTextEditingController(text: 'testuser@email.com');
|
||||
final passwordController = useTextEditingController(text: 'password');
|
||||
final serverEndpointController = useTextEditingController(text: 'http://192.168.1.216:3000');
|
||||
final serverEndpointController = useTextEditingController(text: 'http://192.168.1.103:2283');
|
||||
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/modules/home/views/home_page.dart';
|
||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||
import 'package:immich_mobile/shared/views/backup_controller_page.dart';
|
||||
import 'package:immich_mobile/shared/views/image_viewer_page.dart';
|
||||
import 'package:immich_mobile/shared/views/video_viewer_page.dart';
|
||||
|
||||
part 'router.gr.dart';
|
||||
|
||||
@@ -15,6 +16,7 @@ part 'router.gr.dart';
|
||||
AutoRoute(page: HomePage, guards: [AuthGuard]),
|
||||
AutoRoute(page: BackupControllerPage, guards: [AuthGuard]),
|
||||
AutoRoute(page: ImageViewerPage, guards: [AuthGuard]),
|
||||
AutoRoute(page: VideoViewerPage, guards: [AuthGuard]),
|
||||
],
|
||||
)
|
||||
class AppRouter extends _$AppRouter {
|
||||
|
||||
@@ -42,6 +42,12 @@ class _$AppRouter extends RootStackRouter {
|
||||
imageUrl: args.imageUrl,
|
||||
heroTag: args.heroTag,
|
||||
thumbnailUrl: args.thumbnailUrl));
|
||||
},
|
||||
VideoViewerRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<VideoViewerRouteArgs>();
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: VideoViewerPage(key: args.key, videoUrl: args.videoUrl));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,7 +58,9 @@ class _$AppRouter extends RootStackRouter {
|
||||
RouteConfig(BackupControllerRoute.name,
|
||||
path: '/backup-controller-page', guards: [authGuard]),
|
||||
RouteConfig(ImageViewerRoute.name,
|
||||
path: '/image-viewer-page', guards: [authGuard])
|
||||
path: '/image-viewer-page', guards: [authGuard]),
|
||||
RouteConfig(VideoViewerRoute.name,
|
||||
path: '/video-viewer-page', guards: [authGuard])
|
||||
];
|
||||
}
|
||||
|
||||
@@ -120,3 +128,27 @@ class ImageViewerRouteArgs {
|
||||
return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [VideoViewerPage]
|
||||
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
||||
VideoViewerRoute({Key? key, required String videoUrl})
|
||||
: super(VideoViewerRoute.name,
|
||||
path: '/video-viewer-page',
|
||||
args: VideoViewerRouteArgs(key: key, videoUrl: videoUrl));
|
||||
|
||||
static const String name = 'VideoViewerRoute';
|
||||
}
|
||||
|
||||
class VideoViewerRouteArgs {
|
||||
const VideoViewerRouteArgs({this.key, required this.videoUrl});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final String videoUrl;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,26 +5,22 @@ class ImmichAsset {
|
||||
final String deviceAssetId;
|
||||
final String userId;
|
||||
final String deviceId;
|
||||
final String assetType;
|
||||
final String localPath;
|
||||
final String remotePath;
|
||||
final String type;
|
||||
final String createdAt;
|
||||
final String modifiedAt;
|
||||
final bool isFavorite;
|
||||
final String? description;
|
||||
final String? duration;
|
||||
|
||||
ImmichAsset({
|
||||
required this.id,
|
||||
required this.deviceAssetId,
|
||||
required this.userId,
|
||||
required this.deviceId,
|
||||
required this.assetType,
|
||||
required this.localPath,
|
||||
required this.remotePath,
|
||||
required this.type,
|
||||
required this.createdAt,
|
||||
required this.modifiedAt,
|
||||
required this.isFavorite,
|
||||
this.description,
|
||||
this.duration,
|
||||
});
|
||||
|
||||
ImmichAsset copyWith({
|
||||
@@ -32,26 +28,22 @@ class ImmichAsset {
|
||||
String? deviceAssetId,
|
||||
String? userId,
|
||||
String? deviceId,
|
||||
String? assetType,
|
||||
String? localPath,
|
||||
String? remotePath,
|
||||
String? type,
|
||||
String? createdAt,
|
||||
String? modifiedAt,
|
||||
bool? isFavorite,
|
||||
String? description,
|
||||
String? duration,
|
||||
}) {
|
||||
return ImmichAsset(
|
||||
id: id ?? this.id,
|
||||
deviceAssetId: deviceAssetId ?? this.deviceAssetId,
|
||||
userId: userId ?? this.userId,
|
||||
deviceId: deviceId ?? this.deviceId,
|
||||
assetType: assetType ?? this.assetType,
|
||||
localPath: localPath ?? this.localPath,
|
||||
remotePath: remotePath ?? this.remotePath,
|
||||
type: type ?? this.type,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
modifiedAt: modifiedAt ?? this.modifiedAt,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
description: description ?? this.description,
|
||||
duration: duration ?? this.duration,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,13 +53,11 @@ class ImmichAsset {
|
||||
'deviceAssetId': deviceAssetId,
|
||||
'userId': userId,
|
||||
'deviceId': deviceId,
|
||||
'assetType': assetType,
|
||||
'localPath': localPath,
|
||||
'remotePath': remotePath,
|
||||
'type': type,
|
||||
'createdAt': createdAt,
|
||||
'modifiedAt': modifiedAt,
|
||||
'isFavorite': isFavorite,
|
||||
'description': description,
|
||||
'duration': duration,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,13 +67,11 @@ class ImmichAsset {
|
||||
deviceAssetId: map['deviceAssetId'] ?? '',
|
||||
userId: map['userId'] ?? '',
|
||||
deviceId: map['deviceId'] ?? '',
|
||||
assetType: map['assetType'] ?? '',
|
||||
localPath: map['localPath'] ?? '',
|
||||
remotePath: map['remotePath'] ?? '',
|
||||
type: map['type'] ?? '',
|
||||
createdAt: map['createdAt'] ?? '',
|
||||
modifiedAt: map['modifiedAt'] ?? '',
|
||||
isFavorite: map['isFavorite'] ?? false,
|
||||
description: map['description'],
|
||||
duration: map['duration'],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,7 +81,7 @@ class ImmichAsset {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, assetType: $assetType, localPath: $localPath, remotePath: $remotePath, 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
|
||||
@@ -105,13 +93,11 @@ class ImmichAsset {
|
||||
other.deviceAssetId == deviceAssetId &&
|
||||
other.userId == userId &&
|
||||
other.deviceId == deviceId &&
|
||||
other.assetType == assetType &&
|
||||
other.localPath == localPath &&
|
||||
other.remotePath == remotePath &&
|
||||
other.type == type &&
|
||||
other.createdAt == createdAt &&
|
||||
other.modifiedAt == modifiedAt &&
|
||||
other.isFavorite == isFavorite &&
|
||||
other.description == description;
|
||||
other.duration == duration;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -120,12 +106,10 @@ class ImmichAsset {
|
||||
deviceAssetId.hashCode ^
|
||||
userId.hashCode ^
|
||||
deviceId.hashCode ^
|
||||
assetType.hashCode ^
|
||||
localPath.hashCode ^
|
||||
remotePath.hashCode ^
|
||||
type.hashCode ^
|
||||
createdAt.hashCode ^
|
||||
modifiedAt.hashCode ^
|
||||
isFavorite.hashCode ^
|
||||
description.hashCode;
|
||||
duration.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
void getBackupInfo() async {
|
||||
_updateServerInfo();
|
||||
|
||||
List<AssetPathEntity> list = await PhotoManager.getAssetPathList(onlyAll: true, type: RequestType.image);
|
||||
List<AssetPathEntity> list = await PhotoManager.getAssetPathList(onlyAll: true, type: RequestType.common);
|
||||
|
||||
if (list.isEmpty) {
|
||||
debugPrint("No Asset On Device");
|
||||
@@ -59,7 +59,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
// await PhotoManager.presentLimited();
|
||||
// Gather assets info
|
||||
List<AssetPathEntity> list =
|
||||
await PhotoManager.getAssetPathList(hasAll: true, onlyAll: true, type: RequestType.image);
|
||||
await PhotoManager.getAssetPathList(hasAll: true, onlyAll: true, type: RequestType.common);
|
||||
|
||||
if (list.isEmpty) {
|
||||
debugPrint("No Asset On Device - Abort Backup Process");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
@@ -12,7 +13,6 @@ import 'package:immich_mobile/utils/files_helper.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:exif/exif.dart';
|
||||
|
||||
class BackupService {
|
||||
final NetworkService _networkService = NetworkService();
|
||||
@@ -36,7 +36,11 @@ class BackupService {
|
||||
|
||||
for (var entity in assetList) {
|
||||
try {
|
||||
file = await entity.file.timeout(const Duration(seconds: 5));
|
||||
if (entity.type == AssetType.video) {
|
||||
file = await entity.file;
|
||||
} else {
|
||||
file = await entity.file.timeout(const Duration(seconds: 5));
|
||||
}
|
||||
|
||||
if (file != null) {
|
||||
// reading exif
|
||||
@@ -50,8 +54,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,
|
||||
@@ -60,8 +64,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,
|
||||
|
||||
105
mobile/lib/shared/views/video_viewer_page.dart
Normal file
105
mobile/lib/shared/views/video_viewer_page.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class VideoViewerPage extends StatelessWidget {
|
||||
final String videoUrl;
|
||||
|
||||
const VideoViewerPage({Key? key, required this.videoUrl}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black,
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
AutoRouter.of(context).pop();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back_ios)),
|
||||
),
|
||||
body: Center(
|
||||
child: VideoThumbnailPlayer(
|
||||
url: videoUrl,
|
||||
jwtToken: jwtToken,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoThumbnailPlayer extends StatefulWidget {
|
||||
final String url;
|
||||
final String? jwtToken;
|
||||
|
||||
const VideoThumbnailPlayer({Key? key, required this.url, this.jwtToken}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<VideoThumbnailPlayer> createState() => _VideoThumbnailPlayerState();
|
||||
}
|
||||
|
||||
class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
|
||||
late VideoPlayerController videoPlayerController;
|
||||
ChewieController? chewieController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initializePlayer();
|
||||
}
|
||||
|
||||
Future<void> initializePlayer() async {
|
||||
try {
|
||||
videoPlayerController =
|
||||
VideoPlayerController.network(widget.url, httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"});
|
||||
|
||||
await videoPlayerController.initialize();
|
||||
_createChewieController();
|
||||
setState(() {});
|
||||
} catch (e) {
|
||||
debugPrint("ERROR initialize video player");
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
_createChewieController() {
|
||||
chewieController = ChewieController(
|
||||
showOptions: true,
|
||||
showControlsOnInitialize: false,
|
||||
videoPlayerController: videoPlayerController,
|
||||
autoPlay: true,
|
||||
autoInitialize: false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
videoPlayerController.pause();
|
||||
videoPlayerController.dispose();
|
||||
chewieController?.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return chewieController != null && chewieController!.videoPlayerController.value.isInitialized
|
||||
? SizedBox(
|
||||
child: Chewie(
|
||||
controller: chewieController!,
|
||||
),
|
||||
)
|
||||
: const SizedBox(
|
||||
width: 75,
|
||||
height: 75,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class FileHelper {
|
||||
static getMimeType(String filePath) {
|
||||
debugPrint(filePath);
|
||||
var fileExtension = p.extension(filePath).split(".")[1];
|
||||
|
||||
switch (fileExtension) {
|
||||
switch (fileExtension.toLowerCase()) {
|
||||
case 'gif':
|
||||
return {"type": "image", "subType": "gif"};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user