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:
@@ -3,6 +3,7 @@
|
||||
<application
|
||||
android:label="Immich"
|
||||
android:name="${applicationName}"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Generated file.
|
||||
// If you wish to remove Flutter's multidex support, delete this entire file.
|
||||
|
||||
package io.flutter.app;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.multidex.MultiDex;
|
||||
|
||||
/**
|
||||
* Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support.
|
||||
*/
|
||||
public class FlutterMultiDexApplication extends FlutterApplication {
|
||||
@Override
|
||||
@CallSuper
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(base);
|
||||
MultiDex.install(this);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ PODS:
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- Toast (4.0.0)
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- wakelock (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
@@ -25,6 +29,8 @@ DEPENDENCIES:
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
|
||||
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -44,6 +50,10 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/photo_manager/ios"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/ios"
|
||||
wakelock:
|
||||
:path: ".symlinks/plugins/wakelock/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
||||
@@ -54,6 +64,8 @@ SPEC CHECKSUMS:
|
||||
photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
|
||||
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
||||
|
||||
|
||||
@@ -41,9 +41,22 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true />
|
||||
<true />
|
||||
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>App need your agree, can visit your album</string>
|
||||
<string>We need to manage backup your photos album</string>
|
||||
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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"};
|
||||
|
||||
|
||||
@@ -155,6 +155,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
chewie:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: chewie
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -527,6 +534,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
octo_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -653,6 +667,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -847,6 +868,41 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.18"
|
||||
video_player_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.17"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.18"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
video_player_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
visibility_detector:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -854,6 +910,41 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
wakelock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.6"
|
||||
wakelock_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
wakelock_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
wakelock_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
wakelock_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -898,4 +989,4 @@ packages:
|
||||
version: "3.1.0"
|
||||
sdks:
|
||||
dart: ">=2.15.1 <3.0.0"
|
||||
flutter: ">=2.5.0"
|
||||
flutter: ">=2.8.0"
|
||||
|
||||
@@ -28,6 +28,8 @@ dependencies:
|
||||
visibility_detector: ^0.2.2
|
||||
flutter_launcher_icons: "^0.9.2"
|
||||
fluttertoast: ^8.0.8
|
||||
video_player: ^2.2.18
|
||||
chewie: ^1.2.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user