Merge branch 'main' of github.com:immich-app/immich into feat/show-archived-assets-for-a-person
This commit is contained in:
@@ -10,6 +10,12 @@ abstract interface class IFileMediaRepository {
|
||||
String? relativePath,
|
||||
});
|
||||
|
||||
Future<Asset?> saveImageWithFile(
|
||||
String filePath, {
|
||||
String? title,
|
||||
String? relativePath,
|
||||
});
|
||||
|
||||
Future<Asset?> saveVideo(
|
||||
File file, {
|
||||
required String title,
|
||||
|
||||
@@ -83,7 +83,7 @@ Future<void> initApp() async {
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
debugPrint("FlutterError - Catch all: $error");
|
||||
debugPrint("FlutterError - Catch all: $error \n $stack");
|
||||
log.severe('PlatformDispatcher - Catch all', error, stack);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -61,53 +61,6 @@ class TabControllerPage extends HookConsumerWidget {
|
||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||
}
|
||||
|
||||
navigationRail(TabsRouter tabsRouter) {
|
||||
return NavigationRail(
|
||||
labelType: NavigationRailLabelType.all,
|
||||
selectedIndex: tabsRouter.activeIndex,
|
||||
onDestinationSelected: (index) =>
|
||||
onNavigationSelected(tabsRouter, index),
|
||||
selectedIconTheme: IconThemeData(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
selectedLabelTextStyle: TextStyle(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
useIndicator: false,
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top + 4,
|
||||
left: 4,
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
),
|
||||
icon: const Icon(Icons.photo_library_outlined),
|
||||
selectedIcon: const Icon(Icons.photo_library),
|
||||
label: const Text('tab_controller_nav_photos').tr(),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.search_rounded),
|
||||
selectedIcon: const Icon(Icons.search),
|
||||
label: const Text('tab_controller_nav_search').tr(),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.photo_album_outlined),
|
||||
selectedIcon: const Icon(Icons.photo_album),
|
||||
label: const Text('albums').tr(),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.space_dashboard_outlined),
|
||||
selectedIcon: const Icon(Icons.space_dashboard_rounded),
|
||||
label: const Text('library').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
bottomNavigationBar(TabsRouter tabsRouter) {
|
||||
return NavigationBar(
|
||||
selectedIndex: tabsRouter.activeIndex,
|
||||
@@ -186,33 +139,13 @@ class TabControllerPage extends HookConsumerWidget {
|
||||
canPop: tabsRouter.activeIndex == 0,
|
||||
onPopInvokedWithResult: (didPop, _) =>
|
||||
!didPop ? tabsRouter.setActiveIndex(0) : null,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const medium = 600;
|
||||
final Widget? bottom;
|
||||
final Widget body;
|
||||
if (constraints.maxWidth < medium) {
|
||||
// Normal phone width
|
||||
bottom = bottomNavigationBar(tabsRouter);
|
||||
body = child;
|
||||
} else {
|
||||
// Medium tablet width
|
||||
bottom = null;
|
||||
body = Row(
|
||||
children: [
|
||||
navigationRail(tabsRouter),
|
||||
Expanded(child: child),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
body: HeroControllerScope(
|
||||
controller: HeroController(),
|
||||
child: body,
|
||||
),
|
||||
bottomNavigationBar: multiselectEnabled ? null : bottom,
|
||||
);
|
||||
},
|
||||
child: Scaffold(
|
||||
body: HeroControllerScope(
|
||||
controller: HeroController(),
|
||||
child: child,
|
||||
),
|
||||
bottomNavigationBar:
|
||||
multiselectEnabled ? null : bottomNavigationBar(tabsRouter),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -59,7 +59,7 @@ class LibraryPage extends ConsumerWidget {
|
||||
icon: Icons.link_outlined,
|
||||
label: 'shared_links'.tr(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: trashEnabled ? 8 : 0),
|
||||
trashEnabled
|
||||
? ActionButton(
|
||||
onPressed: () => context.pushRoute(const TrashRoute()),
|
||||
@@ -197,58 +197,65 @@ class PeopleCollectionCard extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final people = ref.watch(getAllPeopleProvider);
|
||||
final size = MediaQuery.of(context).size.width * 0.5 - 20;
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(const PeopleCollectionRoute()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withAlpha(30),
|
||||
context.colorScheme.primary.withAlpha(25),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: people.widgetWhen(
|
||||
onData: (people) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
padding: const EdgeInsets.all(12),
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: people.take(4).map((person) {
|
||||
return CircleAvatar(
|
||||
backgroundImage: NetworkImage(
|
||||
getFaceThumbnailUrl(person.id),
|
||||
headers: ApiService.getRequestHeaders(),
|
||||
),
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isTablet = constraints.maxWidth > 600;
|
||||
final widthFactor = isTablet ? 0.25 : 0.5;
|
||||
final size = MediaQuery.of(context).size.width * widthFactor - 20.0;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(const PeopleCollectionRoute()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withAlpha(30),
|
||||
context.colorScheme.primary.withAlpha(25),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: people.widgetWhen(
|
||||
onData: (people) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
padding: const EdgeInsets.all(12),
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: people.take(4).map((person) {
|
||||
return CircleAvatar(
|
||||
backgroundImage: NetworkImage(
|
||||
getFaceThumbnailUrl(person.id),
|
||||
headers: ApiService.getRequestHeaders(),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'people'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'people'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -260,55 +267,61 @@ class LocalAlbumsCollectionCard extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albums = ref.watch(localAlbumsProvider);
|
||||
|
||||
final size = MediaQuery.of(context).size.width * 0.5 - 20;
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isTablet = constraints.maxWidth > 600;
|
||||
final widthFactor = isTablet ? 0.25 : 0.5;
|
||||
final size = MediaQuery.of(context).size.width * widthFactor - 20.0;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(
|
||||
const LocalAlbumsRoute(),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withAlpha(30),
|
||||
context.colorScheme.primary.withAlpha(25),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
padding: const EdgeInsets.all(12),
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: albums.take(4).map((album) {
|
||||
return AlbumThumbnailCard(
|
||||
album: album,
|
||||
showTitle: false,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(
|
||||
const LocalAlbumsRoute(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'on_this_device'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withAlpha(30),
|
||||
context.colorScheme.primary.withAlpha(25),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
padding: const EdgeInsets.all(12),
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: albums.take(4).map((album) {
|
||||
return AlbumThumbnailCard(
|
||||
album: album,
|
||||
showTitle: false,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'on_this_device'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -317,44 +330,51 @@ class PlacesCollectionCard extends StatelessWidget {
|
||||
const PlacesCollectionCard({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size.width * 0.5 - 20;
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(const PlacesCollectionRoute()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: context.colorScheme.secondaryContainer.withAlpha(100),
|
||||
),
|
||||
child: IgnorePointer(
|
||||
child: MapThumbnail(
|
||||
zoom: 8,
|
||||
centre: const LatLng(
|
||||
21.44950,
|
||||
-157.91959,
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isTablet = constraints.maxWidth > 600;
|
||||
final widthFactor = isTablet ? 0.25 : 0.5;
|
||||
final size = MediaQuery.of(context).size.width * widthFactor - 20.0;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(const PlacesCollectionRoute()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: context.colorScheme.secondaryContainer.withAlpha(100),
|
||||
),
|
||||
child: IgnorePointer(
|
||||
child: MapThumbnail(
|
||||
zoom: 8,
|
||||
centre: const LatLng(
|
||||
21.44950,
|
||||
-157.91959,
|
||||
),
|
||||
showAttribution: false,
|
||||
themeMode:
|
||||
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||
),
|
||||
),
|
||||
showAttribution: false,
|
||||
themeMode:
|
||||
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'places'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'places'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,76 +29,86 @@ class PeopleCollectionPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('people'.tr()),
|
||||
),
|
||||
body: people.when(
|
||||
data: (people) {
|
||||
return GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 0.85,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
itemCount: people.length,
|
||||
itemBuilder: (context, index) {
|
||||
final person = people[index];
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isTablet = constraints.maxWidth > 600;
|
||||
final isPortrait =
|
||||
MediaQuery.of(context).orientation == Orientation.portrait;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.pushRoute(
|
||||
PersonResultRoute(
|
||||
personId: person.id,
|
||||
personName: person.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Material(
|
||||
shape: const CircleBorder(side: BorderSide.none),
|
||||
elevation: 3,
|
||||
child: CircleAvatar(
|
||||
maxRadius: 96 / 2,
|
||||
backgroundImage: NetworkImage(
|
||||
getFaceThumbnailUrl(person.id),
|
||||
headers: headers,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GestureDetector(
|
||||
onTap: () => showNameEditModel(person.id, person.name),
|
||||
child: person.name.isEmpty
|
||||
? Text(
|
||||
'add_a_name'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: context.colorScheme.primary,
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('people'.tr()),
|
||||
),
|
||||
body: people.when(
|
||||
data: (people) {
|
||||
return GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: isTablet ? 6 : 3,
|
||||
childAspectRatio: 0.85,
|
||||
mainAxisSpacing: isPortrait && isTablet ? 36 : 0,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
itemCount: people.length,
|
||||
itemBuilder: (context, index) {
|
||||
final person = people[index];
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.pushRoute(
|
||||
PersonResultRoute(
|
||||
personId: person.id,
|
||||
personName: person.name,
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
person.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Material(
|
||||
shape: const CircleBorder(side: BorderSide.none),
|
||||
elevation: 3,
|
||||
child: CircleAvatar(
|
||||
maxRadius: isTablet ? 120 / 2 : 96 / 2,
|
||||
backgroundImage: NetworkImage(
|
||||
getFaceThumbnailUrl(person.id),
|
||||
headers: headers,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GestureDetector(
|
||||
onTap: () => showNameEditModel(person.id, person.name),
|
||||
child: person.name.isEmpty
|
||||
? Text(
|
||||
'add_a_name'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: Text(
|
||||
person.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
error: (error, stack) => const Text("error"),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
),
|
||||
error: (error, stack) => const Text("error"),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/download/download_state.model.dart';
|
||||
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/download.service.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/services/share.service.dart';
|
||||
@@ -15,10 +16,12 @@ import 'package:immich_mobile/widgets/common/share_dialog.dart';
|
||||
class DownloadStateNotifier extends StateNotifier<DownloadState> {
|
||||
final DownloadService _downloadService;
|
||||
final ShareService _shareService;
|
||||
final AlbumService _albumService;
|
||||
|
||||
DownloadStateNotifier(
|
||||
this._downloadService,
|
||||
this._shareService,
|
||||
this._albumService,
|
||||
) : super(
|
||||
DownloadState(
|
||||
downloadStatus: TaskStatus.complete,
|
||||
@@ -76,7 +79,7 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
|
||||
|
||||
switch (update.status) {
|
||||
case TaskStatus.complete:
|
||||
_downloadService.saveImage(update.task);
|
||||
_downloadService.saveImageWithPath(update.task);
|
||||
_onDownloadComplete(update.task.taskId);
|
||||
break;
|
||||
|
||||
@@ -133,6 +136,7 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
|
||||
showProgress: false,
|
||||
);
|
||||
}
|
||||
_albumService.refreshDeviceAlbums();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -187,5 +191,6 @@ final downloadStateProvider =
|
||||
((ref) => DownloadStateNotifier(
|
||||
ref.watch(downloadServiceProvider),
|
||||
ref.watch(shareServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
)),
|
||||
);
|
||||
|
||||
@@ -25,6 +25,20 @@ class FileMediaRepository implements IFileMediaRepository {
|
||||
return AssetMediaRepository.toAsset(entity);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Asset?> saveImageWithFile(
|
||||
String filePath, {
|
||||
String? title,
|
||||
String? relativePath,
|
||||
}) async {
|
||||
final entity = await PhotoManager.editor.saveImageWithPath(
|
||||
filePath,
|
||||
title: title,
|
||||
relativePath: relativePath,
|
||||
);
|
||||
return AssetMediaRepository.toAsset(entity);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Asset?> saveLivePhoto({
|
||||
required File image,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
@@ -12,6 +12,7 @@ import 'package:immich_mobile/repositories/download.repository.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/download.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final downloadServiceProvider = Provider(
|
||||
(ref) => DownloadService(
|
||||
@@ -23,6 +24,7 @@ final downloadServiceProvider = Provider(
|
||||
class DownloadService {
|
||||
final IDownloadRepository _downloadRepository;
|
||||
final IFileMediaRepository _fileMediaRepository;
|
||||
final Logger _log = Logger("DownloadService");
|
||||
void Function(TaskStatusUpdate)? onImageDownloadStatus;
|
||||
void Function(TaskStatusUpdate)? onVideoDownloadStatus;
|
||||
void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
|
||||
@@ -55,19 +57,25 @@ class DownloadService {
|
||||
onLivePhotoDownloadStatus?.call(update);
|
||||
}
|
||||
|
||||
Future<bool> saveImage(Task task) async {
|
||||
Future<bool> saveImageWithPath(Task task) async {
|
||||
final filePath = await task.filePath();
|
||||
final title = task.filename;
|
||||
final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
|
||||
final data = await File(filePath).readAsBytes();
|
||||
|
||||
final Asset? resultAsset = await _fileMediaRepository.saveImage(
|
||||
data,
|
||||
title: title,
|
||||
relativePath: relativePath,
|
||||
);
|
||||
|
||||
return resultAsset != null;
|
||||
try {
|
||||
final Asset? resultAsset = await _fileMediaRepository.saveImageWithFile(
|
||||
filePath,
|
||||
title: title,
|
||||
relativePath: relativePath,
|
||||
);
|
||||
return resultAsset != null;
|
||||
} catch (error, stack) {
|
||||
_log.severe("Error saving image", error, stack);
|
||||
return false;
|
||||
} finally {
|
||||
if (await File(filePath).exists()) {
|
||||
await File(filePath).delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> saveVideo(Task task) async {
|
||||
@@ -75,58 +83,74 @@ class DownloadService {
|
||||
final title = task.filename;
|
||||
final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
|
||||
final file = File(filePath);
|
||||
|
||||
final Asset? resultAsset = await _fileMediaRepository.saveVideo(
|
||||
file,
|
||||
title: title,
|
||||
relativePath: relativePath,
|
||||
);
|
||||
|
||||
return resultAsset != null;
|
||||
try {
|
||||
final Asset? resultAsset = await _fileMediaRepository.saveVideo(
|
||||
file,
|
||||
title: title,
|
||||
relativePath: relativePath,
|
||||
);
|
||||
return resultAsset != null;
|
||||
} catch (error, stack) {
|
||||
_log.severe("Error saving video", error, stack);
|
||||
return false;
|
||||
} finally {
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> saveLivePhotos(
|
||||
Task task,
|
||||
String livePhotosId,
|
||||
) async {
|
||||
final records = await _downloadRepository.getLiveVideoTasks();
|
||||
if (records.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final imageRecord =
|
||||
_findTaskRecord(records, livePhotosId, LivePhotosPart.image);
|
||||
final videoRecord =
|
||||
_findTaskRecord(records, livePhotosId, LivePhotosPart.video);
|
||||
final imageFilePath = await imageRecord.task.filePath();
|
||||
final videoFilePath = await videoRecord.task.filePath();
|
||||
|
||||
try {
|
||||
final records = await _downloadRepository.getLiveVideoTasks();
|
||||
if (records.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final imageRecord = records.firstWhere(
|
||||
(record) {
|
||||
final metadata = LivePhotosMetadata.fromJson(record.task.metaData);
|
||||
return metadata.id == livePhotosId &&
|
||||
metadata.part == LivePhotosPart.image;
|
||||
},
|
||||
);
|
||||
|
||||
final videoRecord = records.firstWhere((record) {
|
||||
final metadata = LivePhotosMetadata.fromJson(record.task.metaData);
|
||||
return metadata.id == livePhotosId &&
|
||||
metadata.part == LivePhotosPart.video;
|
||||
});
|
||||
|
||||
final imageFilePath = await imageRecord.task.filePath();
|
||||
final videoFilePath = await videoRecord.task.filePath();
|
||||
|
||||
final resultAsset = await _fileMediaRepository.saveLivePhoto(
|
||||
final result = await _fileMediaRepository.saveLivePhoto(
|
||||
image: File(imageFilePath),
|
||||
video: File(videoFilePath),
|
||||
title: task.filename,
|
||||
);
|
||||
|
||||
return result != null;
|
||||
} on PlatformException catch (error, stack) {
|
||||
// Handle saving MotionPhotos on iOS
|
||||
if (error.code == 'PHPhotosErrorDomain (-1)') {
|
||||
final result = await _fileMediaRepository
|
||||
.saveImageWithFile(imageFilePath, title: task.filename);
|
||||
return result != null;
|
||||
}
|
||||
_log.severe("Error saving live photo", error, stack);
|
||||
return false;
|
||||
} catch (error, stack) {
|
||||
_log.severe("Error saving live photo", error, stack);
|
||||
return false;
|
||||
} finally {
|
||||
final imageFile = File(imageFilePath);
|
||||
if (await imageFile.exists()) {
|
||||
await imageFile.delete();
|
||||
}
|
||||
|
||||
final videoFile = File(videoFilePath);
|
||||
if (await videoFile.exists()) {
|
||||
await videoFile.delete();
|
||||
}
|
||||
|
||||
await _downloadRepository.deleteRecordsWithIds([
|
||||
imageRecord.task.taskId,
|
||||
videoRecord.task.taskId,
|
||||
]);
|
||||
|
||||
return resultAsset != null;
|
||||
} catch (error) {
|
||||
debugPrint("Error saving live photo: $error");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +175,9 @@ class DownloadService {
|
||||
await _downloadRepository.download(
|
||||
_buildDownloadTask(
|
||||
asset.livePhotoVideoId!,
|
||||
asset.fileName.toUpperCase().replaceAll(".HEIC", '.MOV'),
|
||||
asset.fileName
|
||||
.toUpperCase()
|
||||
.replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'),
|
||||
group: downloadGroupLivePhoto,
|
||||
metadata: LivePhotosMetadata(
|
||||
part: LivePhotosPart.video,
|
||||
@@ -191,3 +217,14 @@ class DownloadService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TaskRecord _findTaskRecord(
|
||||
List<TaskRecord> records,
|
||||
String livePhotosId,
|
||||
LivePhotosPart part,
|
||||
) {
|
||||
return records.firstWhere((record) {
|
||||
final metadata = LivePhotosMetadata.fromJson(record.task.metaData);
|
||||
return metadata.id == livePhotosId && metadata.part == part;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -138,10 +138,31 @@ class ThumbnailImage extends ConsumerWidget {
|
||||
tag: isFromDto
|
||||
? '${asset.remoteId}-$heroOffset'
|
||||
: asset.id + heroOffset,
|
||||
child: ImmichThumbnail(
|
||||
asset: asset,
|
||||
height: 250,
|
||||
width: 250,
|
||||
child: Stack(
|
||||
children: [
|
||||
ImmichThumbnail(
|
||||
asset: asset,
|
||||
height: 250,
|
||||
width: 250,
|
||||
),
|
||||
Container(
|
||||
height: 250,
|
||||
width: 250,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color.fromRGBO(0, 0, 0, 0.1),
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
Color.fromRGBO(0, 0, 0, 0.1),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
stops: [0, 0.3, 0.6, 1],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -153,11 +174,8 @@ class ThumbnailImage extends ConsumerWidget {
|
||||
color: canDeselect ? assetContainerColor : Colors.grey,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(15.0),
|
||||
bottomRight: Radius.circular(15.0),
|
||||
bottomLeft: Radius.circular(15.0),
|
||||
topLeft: Radius.zero,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(15.0),
|
||||
),
|
||||
child: image,
|
||||
),
|
||||
@@ -177,7 +195,33 @@ class ThumbnailImage extends ConsumerWidget {
|
||||
)
|
||||
: const Border(),
|
||||
),
|
||||
child: buildImage(),
|
||||
child: Stack(
|
||||
children: [
|
||||
buildImage(),
|
||||
if (showStorageIndicator)
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
storageIcon(asset),
|
||||
color: Colors.white.withOpacity(.8),
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
if (asset.isFavorite)
|
||||
const Positioned(
|
||||
left: 8,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
Icons.favorite,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
if (!asset.isImage) buildVideoIcon(),
|
||||
if (asset.stackCount > 0) buildStackIcon(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (multiselectEnabled)
|
||||
Padding(
|
||||
@@ -187,28 +231,6 @@ class ThumbnailImage extends ConsumerWidget {
|
||||
child: buildSelectionIcon(asset),
|
||||
),
|
||||
),
|
||||
if (showStorageIndicator)
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
storageIcon(asset),
|
||||
color: Colors.white.withOpacity(.8),
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
if (asset.isFavorite)
|
||||
const Positioned(
|
||||
left: 8,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
Icons.favorite,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
if (!asset.isImage) buildVideoIcon(),
|
||||
if (asset.stackCount > 0) buildStackIcon(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
populateTestLoginInfo1() {
|
||||
usernameController.text = 'testuser@email.com';
|
||||
passwordController.text = 'password';
|
||||
serverEndpointController.text = 'http://192.168.1.118:2283/api';
|
||||
serverEndpointController.text = 'http://10.1.15.216:2283/api';
|
||||
}
|
||||
|
||||
login() async {
|
||||
|
||||
+4
-4
@@ -1211,18 +1211,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: photo_manager
|
||||
sha256: "32a1ce1095aeaaa792a29f28c1f74613aa75109f21c2d4ab85be3ad9964230a4"
|
||||
sha256: "70159eee32203e8162d49d588232f0299ed3f383c63eef1e899cb6b83dee6b26"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.5.0"
|
||||
version: "3.5.1"
|
||||
photo_manager_image_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: photo_manager_image_provider
|
||||
sha256: "38ef1023dc11de3a8669f16e7c981673b3c5cfee715d17120f4b87daa2cdd0af"
|
||||
sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.2.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ dependencies:
|
||||
sdk: flutter
|
||||
|
||||
path_provider_ios:
|
||||
photo_manager: ^3.5.0
|
||||
photo_manager_image_provider: ^2.1.1
|
||||
photo_manager: ^3.5.1
|
||||
photo_manager_image_provider: ^2.2.0
|
||||
flutter_hooks: ^0.20.4
|
||||
hooks_riverpod: ^2.4.9
|
||||
riverpod_annotation: ^2.3.3
|
||||
|
||||
Reference in New Issue
Block a user