feat(mobile): show local assets (#905)
* introduce Asset as composition of AssetResponseDTO and AssetEntity * filter out duplicate assets (that are both local and remote, take only remote for now) * only allow remote images to be added to albums * introduce ImmichImage to render Asset using local or remote data * optimized deletion of local assets * local video file playback * allow multiple methods to wait on background service finished * skip local assets when adding to album from home screen * fix and optimize delete * show gray box placeholder for local assets * add comments * fix bug: duplicate assets in state after onNewAssetUploaded
This commit is contained in:
parent
99da181cfc
commit
1633af7af6
41 changed files with 830 additions and 514 deletions
|
@ -134,13 +134,13 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopEngine(result: Result?) {
|
private fun stopEngine(result: Result?) {
|
||||||
|
clearBackgroundNotification()
|
||||||
|
engine?.destroy()
|
||||||
|
engine = null
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
Log.d(TAG, "stopEngine result=${result}")
|
Log.d(TAG, "stopEngine result=${result}")
|
||||||
resolvableFuture.set(result)
|
resolvableFuture.set(result)
|
||||||
}
|
}
|
||||||
engine?.destroy()
|
|
||||||
engine = null
|
|
||||||
clearBackgroundNotification()
|
|
||||||
waitOnSetForegroundAsync()
|
waitOnSetForegroundAsync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,10 +35,12 @@ void main() async {
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
Hive.openBox(userInfoBox),
|
Hive.openBox(userInfoBox),
|
||||||
Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
|
Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
|
||||||
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
|
|
||||||
Hive.openBox(hiveGithubReleaseInfoBox),
|
Hive.openBox(hiveGithubReleaseInfoBox),
|
||||||
Hive.openBox(userSettingInfoBox),
|
Hive.openBox(userSettingInfoBox),
|
||||||
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
|
if (!Platform.isAndroid) Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
|
||||||
|
if (!Platform.isAndroid)
|
||||||
|
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
|
||||||
|
if (!Platform.isAndroid) Hive.openBox(backgroundBackupInfoBox),
|
||||||
EasyLocalization.ensureInitialized(),
|
EasyLocalization.ensureInitialized(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -86,8 +88,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||||
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
|
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
|
ref.read(backupProvider.notifier).resumeBackup();
|
||||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||||
ref.watch(backupProvider.notifier).resumeBackup();
|
|
||||||
ref.watch(assetProvider.notifier).getAllAsset();
|
ref.watch(assetProvider.notifier).getAllAsset();
|
||||||
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class AssetSelectionPageResult {
|
class AssetSelectionPageResult {
|
||||||
final Set<AssetResponseDto> selectedNewAsset;
|
final Set<Asset> selectedNewAsset;
|
||||||
final Set<AssetResponseDto> selectedAdditionalAsset;
|
final Set<Asset> selectedAdditionalAsset;
|
||||||
final bool isAlbumExist;
|
final bool isAlbumExist;
|
||||||
|
|
||||||
AssetSelectionPageResult({
|
AssetSelectionPageResult({
|
||||||
|
@ -14,8 +13,8 @@ class AssetSelectionPageResult {
|
||||||
});
|
});
|
||||||
|
|
||||||
AssetSelectionPageResult copyWith({
|
AssetSelectionPageResult copyWith({
|
||||||
Set<AssetResponseDto>? selectedNewAsset,
|
Set<Asset>? selectedNewAsset,
|
||||||
Set<AssetResponseDto>? selectedAdditionalAsset,
|
Set<Asset>? selectedAdditionalAsset,
|
||||||
bool? isAlbumExist,
|
bool? isAlbumExist,
|
||||||
}) {
|
}) {
|
||||||
return AssetSelectionPageResult(
|
return AssetSelectionPageResult(
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class AssetSelectionState {
|
class AssetSelectionState {
|
||||||
final Set<String> selectedMonths;
|
final Set<String> selectedMonths;
|
||||||
final Set<AssetResponseDto> selectedNewAssetsForAlbum;
|
final Set<Asset> selectedNewAssetsForAlbum;
|
||||||
final Set<AssetResponseDto> selectedAdditionalAssetsForAlbum;
|
final Set<Asset> selectedAdditionalAssetsForAlbum;
|
||||||
final Set<AssetResponseDto> selectedAssetsInAlbumViewer;
|
final Set<Asset> selectedAssetsInAlbumViewer;
|
||||||
final bool isMultiselectEnable;
|
final bool isMultiselectEnable;
|
||||||
|
|
||||||
/// Indicate the asset selection page is navigated from existing album
|
/// Indicate the asset selection page is navigated from existing album
|
||||||
|
@ -22,9 +21,9 @@ class AssetSelectionState {
|
||||||
|
|
||||||
AssetSelectionState copyWith({
|
AssetSelectionState copyWith({
|
||||||
Set<String>? selectedMonths,
|
Set<String>? selectedMonths,
|
||||||
Set<AssetResponseDto>? selectedNewAssetsForAlbum,
|
Set<Asset>? selectedNewAssetsForAlbum,
|
||||||
Set<AssetResponseDto>? selectedAdditionalAssetsForAlbum,
|
Set<Asset>? selectedAdditionalAssetsForAlbum,
|
||||||
Set<AssetResponseDto>? selectedAssetsInAlbumViewer,
|
Set<Asset>? selectedAssetsInAlbumViewer,
|
||||||
bool? isMultiselectEnable,
|
bool? isMultiselectEnable,
|
||||||
bool? isAlbumExist,
|
bool? isAlbumExist,
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||||
|
@ -13,7 +14,6 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllAlbums() async {
|
getAllAlbums() async {
|
||||||
|
|
||||||
if (await _albumCacheService.isValid() && state.isEmpty) {
|
if (await _albumCacheService.isValid() && state.isEmpty) {
|
||||||
state = await _albumCacheService.get();
|
state = await _albumCacheService.get();
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||||
|
|
||||||
Future<AlbumResponseDto?> createAlbum(
|
Future<AlbumResponseDto?> createAlbum(
|
||||||
String albumTitle,
|
String albumTitle,
|
||||||
Set<AssetResponseDto> assets,
|
Set<Asset> assets,
|
||||||
) async {
|
) async {
|
||||||
AlbumResponseDto? album =
|
AlbumResponseDto? album =
|
||||||
await _albumService.createAlbum(albumTitle, assets, []);
|
await _albumService.createAlbum(albumTitle, assets, []);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/models/asset_selection_state.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_state.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
AssetSelectionNotifier()
|
AssetSelectionNotifier()
|
||||||
|
@ -22,15 +21,15 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
|
|
||||||
void removeAssetsInMonth(
|
void removeAssetsInMonth(
|
||||||
String removedMonth,
|
String removedMonth,
|
||||||
List<AssetResponseDto> assetsInMonth,
|
List<Asset> assetsInMonth,
|
||||||
) {
|
) {
|
||||||
Set<AssetResponseDto> currentAssetList = state.selectedNewAssetsForAlbum;
|
Set<Asset> currentAssetList = state.selectedNewAssetsForAlbum;
|
||||||
Set<String> currentMonthList = state.selectedMonths;
|
Set<String> currentMonthList = state.selectedMonths;
|
||||||
|
|
||||||
currentMonthList
|
currentMonthList
|
||||||
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
|
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
|
||||||
|
|
||||||
for (AssetResponseDto asset in assetsInMonth) {
|
for (Asset asset in assetsInMonth) {
|
||||||
currentAssetList.removeWhere((e) => e.id == asset.id);
|
currentAssetList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +39,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAdditionalAssets(List<AssetResponseDto> assets) {
|
void addAdditionalAssets(List<Asset> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedAdditionalAssetsForAlbum: {
|
selectedAdditionalAssetsForAlbum: {
|
||||||
...state.selectedAdditionalAssetsForAlbum,
|
...state.selectedAdditionalAssetsForAlbum,
|
||||||
|
@ -49,7 +48,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAllAssetsInMonth(String month, List<AssetResponseDto> assetsInMonth) {
|
void addAllAssetsInMonth(String month, List<Asset> assetsInMonth) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedMonths: {...state.selectedMonths, month},
|
selectedMonths: {...state.selectedMonths, month},
|
||||||
selectedNewAssetsForAlbum: {
|
selectedNewAssetsForAlbum: {
|
||||||
|
@ -59,7 +58,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addNewAssets(List<AssetResponseDto> assets) {
|
void addNewAssets(List<Asset> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedNewAssetsForAlbum: {
|
selectedNewAssetsForAlbum: {
|
||||||
...state.selectedNewAssetsForAlbum,
|
...state.selectedNewAssetsForAlbum,
|
||||||
|
@ -68,20 +67,20 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSelectedNewAssets(List<AssetResponseDto> assets) {
|
void removeSelectedNewAssets(List<Asset> assets) {
|
||||||
Set<AssetResponseDto> currentList = state.selectedNewAssetsForAlbum;
|
Set<Asset> currentList = state.selectedNewAssetsForAlbum;
|
||||||
|
|
||||||
for (AssetResponseDto asset in assets) {
|
for (Asset asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
|
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSelectedAdditionalAssets(List<AssetResponseDto> assets) {
|
void removeSelectedAdditionalAssets(List<Asset> assets) {
|
||||||
Set<AssetResponseDto> currentList = state.selectedAdditionalAssetsForAlbum;
|
Set<Asset> currentList = state.selectedAdditionalAssetsForAlbum;
|
||||||
|
|
||||||
for (AssetResponseDto asset in assets) {
|
for (Asset asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +108,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAssetsInAlbumViewer(List<AssetResponseDto> assets) {
|
void addAssetsInAlbumViewer(List<Asset> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedAssetsInAlbumViewer: {
|
selectedAssetsInAlbumViewer: {
|
||||||
...state.selectedAssetsInAlbumViewer,
|
...state.selectedAssetsInAlbumViewer,
|
||||||
|
@ -118,10 +117,10 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeAssetsInAlbumViewer(List<AssetResponseDto> assets) {
|
void removeAssetsInAlbumViewer(List<Asset> assets) {
|
||||||
Set<AssetResponseDto> currentList = state.selectedAssetsInAlbumViewer;
|
Set<Asset> currentList = state.selectedAssetsInAlbumViewer;
|
||||||
|
|
||||||
for (AssetResponseDto asset in assets) {
|
for (Asset asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||||
SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService) : super([]);
|
SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService)
|
||||||
|
: super([]);
|
||||||
|
|
||||||
final AlbumService _sharedAlbumService;
|
final AlbumService _sharedAlbumService;
|
||||||
final SharedAlbumCacheService _sharedAlbumCacheService;
|
final SharedAlbumCacheService _sharedAlbumCacheService;
|
||||||
|
@ -16,7 +18,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||||
|
|
||||||
Future<AlbumResponseDto?> createSharedAlbum(
|
Future<AlbumResponseDto?> createSharedAlbum(
|
||||||
String albumName,
|
String albumName,
|
||||||
Set<AssetResponseDto> assets,
|
Set<Asset> assets,
|
||||||
List<String> sharedUserIds,
|
List<String> sharedUserIds,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
@ -29,7 +30,7 @@ class AlbumService {
|
||||||
|
|
||||||
Future<AlbumResponseDto?> createAlbum(
|
Future<AlbumResponseDto?> createAlbum(
|
||||||
String albumName,
|
String albumName,
|
||||||
Set<AssetResponseDto> assets,
|
Iterable<Asset> assets,
|
||||||
List<String> sharedUserIds,
|
List<String> sharedUserIds,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
|
@ -65,7 +66,7 @@ class AlbumService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AlbumResponseDto?> createAlbumWithGeneratedName(
|
Future<AlbumResponseDto?> createAlbumWithGeneratedName(
|
||||||
Set<AssetResponseDto> assets,
|
Iterable<Asset> assets,
|
||||||
) async {
|
) async {
|
||||||
return createAlbum(
|
return createAlbum(
|
||||||
_getNextAlbumName(await getAlbums(isShared: false)), assets, []);
|
_getNextAlbumName(await getAlbums(isShared: false)), assets, []);
|
||||||
|
@ -81,7 +82,7 @@ class AlbumService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum(
|
Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum(
|
||||||
Set<AssetResponseDto> assets,
|
Iterable<Asset> assets,
|
||||||
String albumId,
|
String albumId,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
|
|
||||||
class AlbumViewerThumbnail extends HookConsumerWidget {
|
class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
final List<AssetResponseDto> assetList;
|
final List<Asset> assetList;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
|
|
||||||
const AlbumViewerThumbnail({
|
const AlbumViewerThumbnail({
|
||||||
|
@ -24,8 +21,6 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
|
||||||
var thumbnailRequestUrl = getThumbnailUrl(asset);
|
|
||||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||||
final selectedAssetsInAlbumViewer =
|
final selectedAssetsInAlbumViewer =
|
||||||
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
||||||
|
@ -120,27 +115,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
_buildThumbnailImage() {
|
_buildThumbnailImage() {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(border: drawBorderColor()),
|
decoration: BoxDecoration(border: drawBorderColor()),
|
||||||
child: CachedNetworkImage(
|
child: ImmichImage(asset, width: 300, height: 300),
|
||||||
cacheKey: asset.id,
|
|
||||||
width: 300,
|
|
||||||
height: 300,
|
|
||||||
memCacheHeight: 200,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
imageUrl: thumbnailRequestUrl,
|
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
|
||||||
Transform.scale(
|
|
||||||
scale: 0.2,
|
|
||||||
child: CircularProgressIndicator(value: downloadProgress.progress),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) {
|
|
||||||
return Icon(
|
|
||||||
Icons.image_not_supported_outlined,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +142,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
_buildThumbnailImage(),
|
_buildThumbnailImage(),
|
||||||
if (showStorageIndicator) _buildAssetStoreLocationIcon(),
|
if (showStorageIndicator) _buildAssetStoreLocationIcon(),
|
||||||
if (asset.type != AssetTypeEnum.IMAGE) _buildVideoLabel(),
|
if (!asset.isImage) _buildVideoLabel(),
|
||||||
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
|
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/selection_thumbnail_image.dart';
|
import 'package:immich_mobile/modules/album/ui/selection_thumbnail_image.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
class AssetGridByMonth extends HookConsumerWidget {
|
class AssetGridByMonth extends HookConsumerWidget {
|
||||||
final List<AssetResponseDto> assetGroup;
|
final List<Asset> assetGroup;
|
||||||
const AssetGridByMonth({Key? key, required this.assetGroup})
|
const AssetGridByMonth({Key? key, required this.assetGroup})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
class MonthGroupTitle extends HookConsumerWidget {
|
class MonthGroupTitle extends HookConsumerWidget {
|
||||||
final String month;
|
final String month;
|
||||||
final List<AssetResponseDto> assetGroup;
|
final List<Asset> assetGroup;
|
||||||
|
|
||||||
const MonthGroupTitle({
|
const MonthGroupTitle({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
|
|
@ -1,29 +1,24 @@
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
|
|
||||||
class SelectionThumbnailImage extends HookConsumerWidget {
|
class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
|
|
||||||
const SelectionThumbnailImage({Key? key, required this.asset})
|
const SelectionThumbnailImage({Key? key, required this.asset})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
|
||||||
var thumbnailRequestUrl = getThumbnailUrl(asset);
|
|
||||||
var selectedAsset =
|
var selectedAsset =
|
||||||
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
||||||
var newAssetsForAlbum =
|
var newAssetsForAlbum =
|
||||||
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
||||||
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
||||||
|
|
||||||
Widget _buildSelectionIcon(AssetResponseDto asset) {
|
Widget _buildSelectionIcon(Asset asset) {
|
||||||
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
||||||
var isNewlySelected =
|
var isNewlySelected =
|
||||||
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
|
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
|
||||||
|
@ -110,30 +105,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(border: drawBorderColor()),
|
decoration: BoxDecoration(border: drawBorderColor()),
|
||||||
child: CachedNetworkImage(
|
child: ImmichImage(asset, width: 150, height: 150),
|
||||||
cacheKey: asset.id,
|
|
||||||
width: 150,
|
|
||||||
height: 150,
|
|
||||||
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
imageUrl: thumbnailRequestUrl,
|
|
||||||
httpHeaders: {
|
|
||||||
"Authorization": "Bearer ${box.get(accessTokenKey)}"
|
|
||||||
},
|
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
|
||||||
Transform.scale(
|
|
||||||
scale: 0.2,
|
|
||||||
child:
|
|
||||||
CircularProgressIndicator(value: downloadProgress.progress),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) {
|
|
||||||
return Icon(
|
|
||||||
Icons.image_not_supported_outlined,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(3.0),
|
padding: const EdgeInsets.all(3.0),
|
||||||
|
@ -142,7 +114,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
child: _buildSelectionIcon(asset),
|
child: _buildSelectionIcon(asset),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (asset.type != AssetTypeEnum.IMAGE)
|
if (!asset.isImage)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
right: 5,
|
right: 5,
|
||||||
|
|
|
@ -1,49 +1,23 @@
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
|
|
||||||
const SharedAlbumThumbnailImage({Key? key, required this.asset})
|
const SharedAlbumThumbnailImage({Key? key, required this.asset})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// debugPrint("View ${asset.id}");
|
// debugPrint("View ${asset.id}");
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
CachedNetworkImage(
|
ImmichImage(asset, width: 500, height: 500),
|
||||||
cacheKey: asset.id,
|
|
||||||
width: 500,
|
|
||||||
height: 500,
|
|
||||||
memCacheHeight: 500,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
imageUrl: getThumbnailUrl(asset),
|
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
|
||||||
Transform.scale(
|
|
||||||
scale: 0.2,
|
|
||||||
child:
|
|
||||||
CircularProgressIndicator(value: downloadProgress.progress),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) {
|
|
||||||
return Icon(
|
|
||||||
Icons.image_not_supported_outlined,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,6 +16,7 @@ import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
|
@ -38,9 +39,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
/// If they exist, add to selected asset state to show they are already selected.
|
/// If they exist, add to selected asset state to show they are already selected.
|
||||||
void _onAddPhotosPressed(AlbumResponseDto albumInfo) async {
|
void _onAddPhotosPressed(AlbumResponseDto albumInfo) async {
|
||||||
if (albumInfo.assets.isNotEmpty == true) {
|
if (albumInfo.assets.isNotEmpty == true) {
|
||||||
ref
|
ref.watch(assetSelectionProvider.notifier).addNewAssets(
|
||||||
.watch(assetSelectionProvider.notifier)
|
albumInfo.assets.map((e) => Asset.remote(e)).toList(),
|
||||||
.addNewAssets(albumInfo.assets.toList());
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
|
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
|
||||||
|
@ -205,8 +206,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
return AlbumViewerThumbnail(
|
return AlbumViewerThumbnail(
|
||||||
asset: albumInfo.assets[index],
|
asset: Asset.remote(albumInfo.assets[index]),
|
||||||
assetList: albumInfo.assets,
|
assetList:
|
||||||
|
albumInfo.assets.map((e) => Asset.remote(e)).toList(),
|
||||||
showStorageIndicator: showStorageIndicator,
|
showStorageIndicator: showStorageIndicator,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -166,7 +166,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: _onBackgroundTapped,
|
onTap: _onBackgroundTapped,
|
||||||
child: SharedAlbumThumbnailImage(
|
child: SharedAlbumThumbnailImage(
|
||||||
asset: selectedAssets.toList()[index],
|
asset: selectedAssets.elementAt(index),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,7 +18,6 @@ class SharingPage extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
|
|
||||||
final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
|
final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/services/share.service.dart';
|
import 'package:immich_mobile/shared/services/share.service.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
||||||
|
@ -47,7 +47,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
|
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void shareAsset(AssetResponseDto asset, BuildContext context) async {
|
void shareAsset(Asset asset, BuildContext context) async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext buildContext) {
|
builder: (BuildContext buildContext) {
|
||||||
|
|
|
@ -2,12 +2,13 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
class ExifBottomSheet extends ConsumerWidget {
|
class ExifBottomSheet extends ConsumerWidget {
|
||||||
final AssetResponseDto assetDetail;
|
final Asset assetDetail;
|
||||||
|
|
||||||
const ExifBottomSheet({Key? key, required this.assetDetail})
|
const ExifBottomSheet({Key? key, required this.assetDetail})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
@ -26,8 +27,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
child: FlutterMap(
|
child: FlutterMap(
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
center: LatLng(
|
center: LatLng(
|
||||||
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
|
assetDetail.latitude ?? 0,
|
||||||
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
|
assetDetail.longitude ?? 0,
|
||||||
),
|
),
|
||||||
zoom: 16.0,
|
zoom: 16.0,
|
||||||
),
|
),
|
||||||
|
@ -48,8 +49,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
Marker(
|
Marker(
|
||||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||||
point: LatLng(
|
point: LatLng(
|
||||||
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
|
assetDetail.latitude ?? 0,
|
||||||
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
|
assetDetail.longitude ?? 0,
|
||||||
),
|
),
|
||||||
builder: (ctx) => const Image(
|
builder: (ctx) => const Image(
|
||||||
image: AssetImage('assets/location-pin.png'),
|
image: AssetImage('assets/location-pin.png'),
|
||||||
|
@ -63,9 +64,11 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExifResponseDto? exifInfo = assetDetail.remote?.exifInfo;
|
||||||
|
|
||||||
_buildLocationText() {
|
_buildLocationText() {
|
||||||
return Text(
|
return Text(
|
||||||
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
"${exifInfo?.city}, ${exifInfo?.state}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
|
@ -78,10 +81,10 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
if (assetDetail.exifInfo?.dateTimeOriginal != null)
|
if (exifInfo?.dateTimeOriginal != null)
|
||||||
Text(
|
Text(
|
||||||
DateFormat('date_format'.tr()).format(
|
DateFormat('date_format'.tr()).format(
|
||||||
assetDetail.exifInfo!.dateTimeOriginal!.toLocal(),
|
exifInfo!.dateTimeOriginal!.toLocal(),
|
||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey[400],
|
color: Colors.grey[400],
|
||||||
|
@ -101,7 +104,7 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
|
|
||||||
// Location
|
// Location
|
||||||
if (assetDetail.exifInfo?.latitude != null)
|
if (assetDetail.latitude != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 32.0),
|
padding: const EdgeInsets.only(top: 32.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -115,21 +118,22 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
"exif_bottom_sheet_location",
|
"exif_bottom_sheet_location",
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
||||||
).tr(),
|
).tr(),
|
||||||
if (assetDetail.exifInfo?.latitude != null &&
|
if (assetDetail.latitude != null &&
|
||||||
assetDetail.exifInfo?.longitude != null)
|
assetDetail.longitude != null)
|
||||||
_buildMap(),
|
_buildMap(),
|
||||||
if (assetDetail.exifInfo?.city != null &&
|
if (exifInfo != null &&
|
||||||
assetDetail.exifInfo?.state != null)
|
exifInfo.city != null &&
|
||||||
|
exifInfo.state != null)
|
||||||
_buildLocationText(),
|
_buildLocationText(),
|
||||||
Text(
|
Text(
|
||||||
"${assetDetail.exifInfo?.latitude?.toStringAsFixed(4)}, ${assetDetail.exifInfo?.longitude?.toStringAsFixed(4)}",
|
"${assetDetail.latitude?.toStringAsFixed(4)}, ${assetDetail.longitude?.toStringAsFixed(4)}",
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Detail
|
// Detail
|
||||||
if (assetDetail.exifInfo != null)
|
if (exifInfo != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 32.0),
|
padding: const EdgeInsets.only(top: 32.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -153,16 +157,16 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
iconColor: Colors.grey[300],
|
iconColor: Colors.grey[300],
|
||||||
leading: const Icon(Icons.image),
|
leading: const Icon(Icons.image),
|
||||||
title: Text(
|
title: Text(
|
||||||
"${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
|
"${exifInfo.imageName!}${p.extension(assetDetail.remote!.originalPath)}",
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
subtitle: assetDetail.exifInfo?.exifImageHeight != null
|
subtitle: exifInfo.exifImageHeight != null
|
||||||
? Text(
|
? Text(
|
||||||
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ",
|
"${exifInfo.exifImageHeight} x ${exifInfo.exifImageWidth} ${exifInfo.fileSizeInByte!}B ",
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
if (assetDetail.exifInfo?.make != null)
|
if (exifInfo.make != null)
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.all(0),
|
contentPadding: const EdgeInsets.all(0),
|
||||||
dense: true,
|
dense: true,
|
||||||
|
@ -170,11 +174,11 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
iconColor: Colors.grey[300],
|
iconColor: Colors.grey[300],
|
||||||
leading: const Icon(Icons.camera),
|
leading: const Icon(Icons.camera),
|
||||||
title: Text(
|
title: Text(
|
||||||
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
|
"${exifInfo.make} ${exifInfo.model}",
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} ",
|
"ƒ/${exifInfo.fNumber} 1/${(1 / (exifInfo.exposureTime ?? 1)).toStringAsFixed(0)} ${exifInfo.focalLength}mm ISO${exifInfo.iso} ",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart'
|
||||||
|
show AssetEntityImageProvider, ThumbnailSize;
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
|
||||||
enum _RemoteImageStatus { empty, thumbnail, preview, full }
|
enum _RemoteImageStatus { empty, thumbnail, preview, full }
|
||||||
|
|
||||||
class _RemotePhotoViewState extends State<RemotePhotoView> {
|
class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
late CachedNetworkImageProvider _imageProvider;
|
late ImageProvider _imageProvider;
|
||||||
_RemoteImageStatus _status = _RemoteImageStatus.empty;
|
_RemoteImageStatus _status = _RemoteImageStatus.empty;
|
||||||
bool _zoomedIn = false;
|
bool _zoomedIn = false;
|
||||||
|
|
||||||
late CachedNetworkImageProvider fullProvider;
|
late ImageProvider _fullProvider;
|
||||||
late CachedNetworkImageProvider previewProvider;
|
late ImageProvider _previewProvider;
|
||||||
late CachedNetworkImageProvider thumbnailProvider;
|
late ImageProvider _thumbnailProvider;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -68,7 +73,7 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
|
|
||||||
void _performStateTransition(
|
void _performStateTransition(
|
||||||
_RemoteImageStatus newStatus,
|
_RemoteImageStatus newStatus,
|
||||||
CachedNetworkImageProvider provider,
|
ImageProvider provider,
|
||||||
) {
|
) {
|
||||||
if (_status == newStatus) return;
|
if (_status == newStatus) return;
|
||||||
|
|
||||||
|
@ -90,40 +95,58 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _loadImages() {
|
void _loadImages() {
|
||||||
thumbnailProvider = _authorizedImageProvider(
|
if (widget.asset.isLocal) {
|
||||||
widget.thumbnailUrl,
|
_imageProvider = AssetEntityImageProvider(
|
||||||
widget.cacheKey,
|
widget.asset.local!,
|
||||||
);
|
isOriginal: false,
|
||||||
_imageProvider = thumbnailProvider;
|
thumbnailSize: const ThumbnailSize.square(250),
|
||||||
|
);
|
||||||
|
_fullProvider = AssetEntityImageProvider(widget.asset.local!);
|
||||||
|
_fullProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
|
ImageStreamListener((ImageInfo image, _) {
|
||||||
|
_performStateTransition(
|
||||||
|
_RemoteImageStatus.full,
|
||||||
|
_fullProvider,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
thumbnailProvider.resolve(const ImageConfiguration()).addListener(
|
_thumbnailProvider = _authorizedImageProvider(
|
||||||
|
getThumbnailUrl(widget.asset.remote!),
|
||||||
|
widget.asset.id,
|
||||||
|
);
|
||||||
|
_imageProvider = _thumbnailProvider;
|
||||||
|
|
||||||
|
_thumbnailProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
ImageStreamListener((ImageInfo imageInfo, _) {
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
_performStateTransition(
|
_performStateTransition(
|
||||||
_RemoteImageStatus.thumbnail,
|
_RemoteImageStatus.thumbnail,
|
||||||
thumbnailProvider,
|
_thumbnailProvider,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.previewUrl != null) {
|
if (widget.threeStageLoading) {
|
||||||
previewProvider = _authorizedImageProvider(
|
_previewProvider = _authorizedImageProvider(
|
||||||
widget.previewUrl!,
|
getThumbnailUrl(widget.asset.remote!, type: ThumbnailFormat.JPEG),
|
||||||
"${widget.cacheKey}_previewStage",
|
"${widget.asset.id}_previewStage",
|
||||||
);
|
);
|
||||||
previewProvider.resolve(const ImageConfiguration()).addListener(
|
_previewProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
ImageStreamListener((ImageInfo imageInfo, _) {
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
_performStateTransition(_RemoteImageStatus.preview, previewProvider);
|
_performStateTransition(_RemoteImageStatus.preview, _previewProvider);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fullProvider = _authorizedImageProvider(
|
_fullProvider = _authorizedImageProvider(
|
||||||
widget.imageUrl,
|
getImageUrl(widget.asset.remote!),
|
||||||
"${widget.cacheKey}_fullStage",
|
"${widget.asset.id}_fullStage",
|
||||||
);
|
);
|
||||||
fullProvider.resolve(const ImageConfiguration()).addListener(
|
_fullProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
ImageStreamListener((ImageInfo imageInfo, _) {
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
_performStateTransition(_RemoteImageStatus.full, _fullProvider);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -139,11 +162,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
||||||
if (_status == _RemoteImageStatus.full) {
|
if (_status == _RemoteImageStatus.full) {
|
||||||
await fullProvider.evict();
|
await _fullProvider.evict();
|
||||||
} else if (_status == _RemoteImageStatus.preview) {
|
} else if (_status == _RemoteImageStatus.preview) {
|
||||||
await previewProvider.evict();
|
await _previewProvider.evict();
|
||||||
} else if (_status == _RemoteImageStatus.thumbnail) {
|
} else if (_status == _RemoteImageStatus.thumbnail) {
|
||||||
await thumbnailProvider.evict();
|
await _thumbnailProvider.evict();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _imageProvider.evict();
|
await _imageProvider.evict();
|
||||||
|
@ -153,23 +176,18 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
class RemotePhotoView extends StatefulWidget {
|
class RemotePhotoView extends StatefulWidget {
|
||||||
const RemotePhotoView({
|
const RemotePhotoView({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.thumbnailUrl,
|
required this.asset,
|
||||||
required this.imageUrl,
|
|
||||||
required this.authToken,
|
required this.authToken,
|
||||||
|
required this.threeStageLoading,
|
||||||
required this.isZoomedFunction,
|
required this.isZoomedFunction,
|
||||||
required this.isZoomedListener,
|
required this.isZoomedListener,
|
||||||
required this.onSwipeDown,
|
required this.onSwipeDown,
|
||||||
required this.onSwipeUp,
|
required this.onSwipeUp,
|
||||||
this.previewUrl,
|
|
||||||
required this.cacheKey,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String thumbnailUrl;
|
final Asset asset;
|
||||||
final String imageUrl;
|
|
||||||
final String authToken;
|
final String authToken;
|
||||||
final String? previewUrl;
|
final bool threeStageLoading;
|
||||||
final String cacheKey;
|
|
||||||
|
|
||||||
final void Function() onSwipeDown;
|
final void Function() onSwipeDown;
|
||||||
final void Function() onSwipeUp;
|
final void Function() onSwipeUp;
|
||||||
final void Function() isZoomedFunction;
|
final void Function() isZoomedFunction;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||||
const TopControlAppBar({
|
const TopControlAppBar({
|
||||||
|
@ -13,9 +13,9 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||||
this.loading = false,
|
this.loading = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
final Function onMoreInfoPressed;
|
final Function onMoreInfoPressed;
|
||||||
final Function onDownloadPressed;
|
final VoidCallback? onDownloadPressed;
|
||||||
final Function onSharePressed;
|
final Function onSharePressed;
|
||||||
final bool loading;
|
final bool loading;
|
||||||
|
|
||||||
|
@ -47,17 +47,16 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||||
child: const CircularProgressIndicator(strokeWidth: 2.0),
|
child: const CircularProgressIndicator(strokeWidth: 2.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
if (!asset.isLocal)
|
||||||
iconSize: iconSize,
|
IconButton(
|
||||||
splashRadius: iconSize,
|
iconSize: iconSize,
|
||||||
onPressed: () {
|
splashRadius: iconSize,
|
||||||
onDownloadPressed();
|
onPressed: onDownloadPressed,
|
||||||
},
|
icon: Icon(
|
||||||
icon: Icon(
|
Icons.cloud_download_rounded,
|
||||||
Icons.cloud_download_rounded,
|
color: Colors.grey[200],
|
||||||
color: Colors.grey[200],
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
|
@ -69,17 +68,18 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
if (asset.isRemote)
|
||||||
iconSize: iconSize,
|
IconButton(
|
||||||
splashRadius: iconSize,
|
iconSize: iconSize,
|
||||||
onPressed: () {
|
splashRadius: iconSize,
|
||||||
onMoreInfoPressed();
|
onPressed: () {
|
||||||
},
|
onMoreInfoPressed();
|
||||||
icon: Icon(
|
},
|
||||||
Icons.more_horiz_rounded,
|
icon: Icon(
|
||||||
color: Colors.grey[200],
|
Icons.more_horiz_rounded,
|
||||||
),
|
color: Colors.grey[200],
|
||||||
)
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,12 @@ import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class GalleryViewerPage extends HookConsumerWidget {
|
class GalleryViewerPage extends HookConsumerWidget {
|
||||||
late List<AssetResponseDto> assetList;
|
late List<Asset> assetList;
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
|
|
||||||
GalleryViewerPage({
|
GalleryViewerPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
@ -27,7 +27,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
required this.asset,
|
required this.asset,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
AssetResponseDto? assetDetail;
|
Asset? assetDetail;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
@ -37,8 +37,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
final loading = useState(false);
|
final loading = useState(false);
|
||||||
final isZoomed = useState<bool>(false);
|
final isZoomed = useState<bool>(false);
|
||||||
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
|
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
|
||||||
|
final indexOfAsset = useState(assetList.indexOf(asset));
|
||||||
int indexOfAsset = assetList.indexOf(asset);
|
|
||||||
|
|
||||||
PageController controller =
|
PageController controller =
|
||||||
PageController(initialPage: assetList.indexOf(asset));
|
PageController(initialPage: assetList.indexOf(asset));
|
||||||
|
@ -52,15 +51,15 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
|
||||||
initState(int index) {
|
|
||||||
indexOfAsset = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAssetExif() async {
|
getAssetExif() async {
|
||||||
assetDetail = await ref
|
if (assetList[indexOfAsset.value].isRemote) {
|
||||||
.watch(assetServiceProvider)
|
assetDetail = await ref
|
||||||
.getAssetById(assetList[indexOfAsset].id);
|
.watch(assetServiceProvider)
|
||||||
|
.getAssetById(assetList[indexOfAsset.value].id);
|
||||||
|
} else {
|
||||||
|
// TODO local exif parsing?
|
||||||
|
assetDetail = assetList[indexOfAsset.value];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showInfo() {
|
void showInfo() {
|
||||||
|
@ -88,19 +87,20 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
appBar: TopControlAppBar(
|
appBar: TopControlAppBar(
|
||||||
loading: loading.value,
|
loading: loading.value,
|
||||||
asset: assetList[indexOfAsset],
|
asset: assetList[indexOfAsset.value],
|
||||||
onMoreInfoPressed: () {
|
onMoreInfoPressed: () {
|
||||||
showInfo();
|
showInfo();
|
||||||
},
|
},
|
||||||
onDownloadPressed: () {
|
onDownloadPressed: assetList[indexOfAsset.value].isLocal
|
||||||
ref
|
? null
|
||||||
.watch(imageViewerStateProvider.notifier)
|
: () {
|
||||||
.downloadAsset(assetList[indexOfAsset], context);
|
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
||||||
},
|
assetList[indexOfAsset.value].remote!, context);
|
||||||
|
},
|
||||||
onSharePressed: () {
|
onSharePressed: () {
|
||||||
ref
|
ref
|
||||||
.watch(imageViewerStateProvider.notifier)
|
.watch(imageViewerStateProvider.notifier)
|
||||||
.shareAsset(assetList[indexOfAsset], context);
|
.shareAsset(assetList[indexOfAsset.value], context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
@ -113,14 +113,13 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
itemCount: assetList.length,
|
itemCount: assetList.length,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
onPageChanged: (value) {
|
onPageChanged: (value) {
|
||||||
|
indexOfAsset.value = value;
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
},
|
},
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
initState(index);
|
|
||||||
|
|
||||||
getAssetExif();
|
getAssetExif();
|
||||||
|
|
||||||
if (assetList[index].type == AssetTypeEnum.IMAGE) {
|
if (assetList[index].isImage) {
|
||||||
return ImageViewerPage(
|
return ImageViewerPage(
|
||||||
authToken: 'Bearer ${box.get(accessTokenKey)}',
|
authToken: 'Bearer ${box.get(accessTokenKey)}',
|
||||||
isZoomedFunction: isZoomedMethod,
|
isZoomedFunction: isZoomedMethod,
|
||||||
|
@ -139,11 +138,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: assetList[index].id,
|
tag: assetList[index].id,
|
||||||
child: VideoViewerPage(
|
child: VideoViewerPage(asset: assetList[index]),
|
||||||
asset: assetList[index],
|
|
||||||
videoUrl:
|
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,12 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class ImageViewerPage extends HookConsumerWidget {
|
class ImageViewerPage extends HookConsumerWidget {
|
||||||
final String heroTag;
|
final String heroTag;
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
final String authToken;
|
final String authToken;
|
||||||
final ValueNotifier<bool> isZoomedListener;
|
final ValueNotifier<bool> isZoomedListener;
|
||||||
final void Function() isZoomedFunction;
|
final void Function() isZoomedFunction;
|
||||||
|
@ -30,7 +29,7 @@ class ImageViewerPage extends HookConsumerWidget {
|
||||||
required this.threeStageLoading,
|
required this.threeStageLoading,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
AssetResponseDto? assetDetail;
|
Asset? assetDetail;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
@ -38,8 +37,13 @@ class ImageViewerPage extends HookConsumerWidget {
|
||||||
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||||
|
|
||||||
getAssetExif() async {
|
getAssetExif() async {
|
||||||
assetDetail =
|
if (asset.isRemote) {
|
||||||
await ref.watch(assetServiceProvider).getAssetById(asset.id);
|
assetDetail =
|
||||||
|
await ref.watch(assetServiceProvider).getAssetById(asset.id);
|
||||||
|
} else {
|
||||||
|
// TODO local exif parsing?
|
||||||
|
assetDetail = asset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
@ -68,17 +72,13 @@ class ImageViewerPage extends HookConsumerWidget {
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: heroTag,
|
tag: heroTag,
|
||||||
child: RemotePhotoView(
|
child: RemotePhotoView(
|
||||||
thumbnailUrl: getThumbnailUrl(asset),
|
asset: asset,
|
||||||
cacheKey: asset.id,
|
|
||||||
imageUrl: getImageUrl(asset),
|
|
||||||
previewUrl: threeStageLoading
|
|
||||||
? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG)
|
|
||||||
: null,
|
|
||||||
authToken: authToken,
|
authToken: authToken,
|
||||||
|
threeStageLoading: threeStageLoading,
|
||||||
isZoomedFunction: isZoomedFunction,
|
isZoomedFunction: isZoomedFunction,
|
||||||
isZoomedListener: isZoomedListener,
|
isZoomedListener: isZoomedListener,
|
||||||
onSwipeDown: () => AutoRouter.of(context).pop(),
|
onSwipeDown: () => AutoRouter.of(context).pop(),
|
||||||
onSwipeUp: () => showInfo(),
|
onSwipeUp: asset.isRemote ? showInfo : () {},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
@ -6,24 +8,41 @@ import 'package:chewie/chewie.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class VideoViewerPage extends HookConsumerWidget {
|
class VideoViewerPage extends HookConsumerWidget {
|
||||||
final String videoUrl;
|
final Asset asset;
|
||||||
final AssetResponseDto asset;
|
|
||||||
AssetResponseDto? assetDetail;
|
|
||||||
|
|
||||||
VideoViewerPage({Key? key, required this.videoUrl, required this.asset})
|
const VideoViewerPage({Key? key, required this.asset}) : super(key: key);
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
if (asset.isLocal) {
|
||||||
|
final AsyncValue<File> videoFile = ref.watch(_fileFamily(asset.local!));
|
||||||
|
return videoFile.when(
|
||||||
|
data: (data) => VideoThumbnailPlayer(file: data),
|
||||||
|
error: (error, stackTrace) => Icon(
|
||||||
|
Icons.image_not_supported_outlined,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
loading: () => const Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 75,
|
||||||
|
height: 75,
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
final downloadAssetStatus =
|
final downloadAssetStatus =
|
||||||
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||||
|
final box = Hive.box(userInfoBox);
|
||||||
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
|
final String jwtToken = box.get(accessTokenKey);
|
||||||
|
final String videoUrl =
|
||||||
|
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}';
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
|
@ -40,11 +59,21 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VideoThumbnailPlayer extends StatefulWidget {
|
final _fileFamily =
|
||||||
final String url;
|
FutureProvider.family<File, AssetEntity>((ref, entity) async {
|
||||||
final String? jwtToken;
|
final file = await entity.file;
|
||||||
|
if (file == null) {
|
||||||
|
throw Exception();
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
});
|
||||||
|
|
||||||
const VideoThumbnailPlayer({Key? key, required this.url, this.jwtToken})
|
class VideoThumbnailPlayer extends StatefulWidget {
|
||||||
|
final String? url;
|
||||||
|
final String? jwtToken;
|
||||||
|
final File? file;
|
||||||
|
|
||||||
|
const VideoThumbnailPlayer({Key? key, this.url, this.jwtToken, this.file})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -63,10 +92,12 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
|
||||||
|
|
||||||
Future<void> initializePlayer() async {
|
Future<void> initializePlayer() async {
|
||||||
try {
|
try {
|
||||||
videoPlayerController = VideoPlayerController.network(
|
videoPlayerController = widget.file == null
|
||||||
widget.url,
|
? VideoPlayerController.network(
|
||||||
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
|
widget.url!,
|
||||||
);
|
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
|
||||||
|
)
|
||||||
|
: VideoPlayerController.file(widget.file!);
|
||||||
|
|
||||||
await videoPlayerController.initialize();
|
await videoPlayerController.initialize();
|
||||||
_createChewieController();
|
_createChewieController();
|
||||||
|
|
|
@ -50,6 +50,11 @@ class BackgroundService {
|
||||||
_Throttle(_updateProgress, notifyInterval);
|
_Throttle(_updateProgress, notifyInterval);
|
||||||
late final _Throttle _throttledDetailNotify =
|
late final _Throttle _throttledDetailNotify =
|
||||||
_Throttle(_updateDetailProgress, notifyInterval);
|
_Throttle(_updateDetailProgress, notifyInterval);
|
||||||
|
Completer<bool> _hasAccessCompleter = Completer();
|
||||||
|
late Future<bool> _hasAccess =
|
||||||
|
Platform.isAndroid ? _hasAccessCompleter.future : Future.value(true);
|
||||||
|
|
||||||
|
Future<bool> get hasAccess => _hasAccess;
|
||||||
|
|
||||||
bool get isBackgroundInitialized {
|
bool get isBackgroundInitialized {
|
||||||
return _isBackgroundInitialized;
|
return _isBackgroundInitialized;
|
||||||
|
@ -201,6 +206,15 @@ class BackgroundService {
|
||||||
if (!Platform.isAndroid) {
|
if (!Platform.isAndroid) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (_hasLock) {
|
||||||
|
debugPrint("WARNING: [acquireLock] called more than once");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (_hasAccessCompleter.isCompleted) {
|
||||||
|
debugPrint("WARNING: [acquireLock] _hasAccessCompleter is completed");
|
||||||
|
_hasAccessCompleter = Completer();
|
||||||
|
_hasAccess = _hasAccessCompleter.future;
|
||||||
|
}
|
||||||
final int lockTime = Timeline.now;
|
final int lockTime = Timeline.now;
|
||||||
_wantsLockTime = lockTime;
|
_wantsLockTime = lockTime;
|
||||||
final ReceivePort rp = ReceivePort(_portNameLock);
|
final ReceivePort rp = ReceivePort(_portNameLock);
|
||||||
|
@ -219,6 +233,7 @@ class BackgroundService {
|
||||||
}
|
}
|
||||||
_hasLock = true;
|
_hasLock = true;
|
||||||
rp.listen(_heartbeatListener);
|
rp.listen(_heartbeatListener);
|
||||||
|
_hasAccessCompleter.complete(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,6 +286,8 @@ class BackgroundService {
|
||||||
}
|
}
|
||||||
_wantsLockTime = 0;
|
_wantsLockTime = 0;
|
||||||
if (_hasLock) {
|
if (_hasLock) {
|
||||||
|
_hasAccessCompleter = Completer();
|
||||||
|
_hasAccess = _hasAccessCompleter.future;
|
||||||
IsolateNameServer.removePortNameMapping(_portNameLock);
|
IsolateNameServer.removePortNameMapping(_portNameLock);
|
||||||
_waitingIsolate?.send(true);
|
_waitingIsolate?.send(true);
|
||||||
_waitingIsolate = null;
|
_waitingIsolate = null;
|
||||||
|
|
|
@ -46,6 +46,17 @@ class HiveBackupAlbums {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a deep copy to allow safe modification without changing the global
|
||||||
|
/// state of [HiveBackupAlbums] before actually saving the changes
|
||||||
|
HiveBackupAlbums deepCopy() {
|
||||||
|
return HiveBackupAlbums(
|
||||||
|
selectedAlbumIds: selectedAlbumIds.toList(),
|
||||||
|
excludedAlbumsIds: excludedAlbumsIds.toList(),
|
||||||
|
lastSelectedBackupTime: lastSelectedBackupTime.toList(),
|
||||||
|
lastExcludedBackupTime: lastExcludedBackupTime.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
final result = <String, dynamic>{};
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
|
|
|
@ -565,11 +565,16 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
|
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
|
||||||
final bool hasLock = await _backgroundService.acquireLock();
|
final bool hasLock = await _backgroundService.acquireLock();
|
||||||
if (!hasLock) {
|
if (!hasLock) {
|
||||||
|
debugPrint("WARNING [resumeBackup] failed to acquireLock");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Box<HiveBackupAlbums> box =
|
await Future.wait([
|
||||||
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
|
||||||
HiveBackupAlbums? albums = box.get(backupInfoKey);
|
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
|
||||||
|
Hive.openBox(backgroundBackupInfoBox),
|
||||||
|
]);
|
||||||
|
final HiveBackupAlbums? albums =
|
||||||
|
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).get(backupInfoKey);
|
||||||
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
||||||
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
||||||
if (albums != null) {
|
if (albums != null) {
|
||||||
|
@ -584,8 +589,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
albums.lastExcludedBackupTime,
|
albums.lastExcludedBackupTime,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox);
|
final Box backgroundBox = Hive.box(backgroundBackupInfoBox);
|
||||||
final Box backgroundBox = await Hive.openBox(backgroundBackupInfoBox);
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backupProgress: previous,
|
backupProgress: previous,
|
||||||
selectedBackupAlbums: selectedAlbums,
|
selectedBackupAlbums: selectedAlbums,
|
||||||
|
|
|
@ -1,34 +1,90 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:photo_manager/src/types/entity.dart';
|
||||||
|
|
||||||
final assetServiceProvider = Provider(
|
final assetServiceProvider = Provider(
|
||||||
(ref) => AssetService(
|
(ref) => AssetService(
|
||||||
ref.watch(apiServiceProvider),
|
ref.watch(apiServiceProvider),
|
||||||
|
ref.watch(backupServiceProvider),
|
||||||
|
ref.watch(backgroundServiceProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
class AssetService {
|
class AssetService {
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
|
final BackupService _backupService;
|
||||||
|
final BackgroundService _backgroundService;
|
||||||
|
|
||||||
AssetService(this._apiService);
|
AssetService(this._apiService, this._backupService, this._backgroundService);
|
||||||
|
|
||||||
Future<List<AssetResponseDto>?> getAllAsset() async {
|
/// Returns all local, remote assets in that order
|
||||||
|
Future<List<Asset>> getAllAsset({bool urgent = false}) async {
|
||||||
|
final List<Asset> assets = [];
|
||||||
try {
|
try {
|
||||||
return await _apiService.assetApi.getAllAssets();
|
// not using `await` here to fetch local & remote assets concurrently
|
||||||
|
final Future<List<AssetResponseDto>?> remoteTask =
|
||||||
|
_apiService.assetApi.getAllAssets();
|
||||||
|
final Iterable<AssetEntity> newLocalAssets;
|
||||||
|
final List<AssetEntity> localAssets = await _getLocalAssets(urgent);
|
||||||
|
final List<AssetResponseDto> remoteAssets = await remoteTask ?? [];
|
||||||
|
if (remoteAssets.isNotEmpty && localAssets.isNotEmpty) {
|
||||||
|
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
|
final Set<String> existingIds = remoteAssets
|
||||||
|
.where((e) => e.deviceId == deviceId)
|
||||||
|
.map((e) => e.deviceAssetId)
|
||||||
|
.toSet();
|
||||||
|
newLocalAssets = localAssets.where((e) => !existingIds.contains(e.id));
|
||||||
|
} else {
|
||||||
|
newLocalAssets = localAssets;
|
||||||
|
}
|
||||||
|
|
||||||
|
assets.addAll(newLocalAssets.map((e) => Asset.local(e)));
|
||||||
|
// the order (first all local, then remote assets) is important!
|
||||||
|
assets.addAll(remoteAssets.map((e) => Asset.remote(e)));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error [getAllAsset] ${e.toString()}");
|
debugPrint("Error [getAllAsset] ${e.toString()}");
|
||||||
return null;
|
}
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// if [urgent] is `true`, do not block by waiting on the background service
|
||||||
|
/// to finish running. Returns an empty list instead after a timeout.
|
||||||
|
Future<List<AssetEntity>> _getLocalAssets(bool urgent) async {
|
||||||
|
try {
|
||||||
|
final Future<bool> hasAccess = urgent
|
||||||
|
? _backgroundService.hasAccess
|
||||||
|
.timeout(const Duration(milliseconds: 250))
|
||||||
|
: _backgroundService.hasAccess;
|
||||||
|
if (!await hasAccess) {
|
||||||
|
throw Exception("Error [getAllAsset] failed to gain access");
|
||||||
|
}
|
||||||
|
final box = await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
|
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
|
||||||
|
|
||||||
|
return backupAlbumInfo != null
|
||||||
|
? await _backupService
|
||||||
|
.buildUploadCandidates(backupAlbumInfo.deepCopy())
|
||||||
|
: [];
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error [_getLocalAssets] ${e.toString()}");
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AssetResponseDto?> getAssetById(String assetId) async {
|
Future<Asset?> getAssetById(String assetId) async {
|
||||||
try {
|
try {
|
||||||
return await _apiService.assetApi.getAssetById(assetId);
|
return Asset.remote(await _apiService.assetApi.getAssetById(assetId));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error [getAssetById] ${e.toString()}");
|
debugPrint("Error [getAssetById] ${e.toString()}");
|
||||||
return null;
|
return null;
|
||||||
|
@ -36,12 +92,12 @@ class AssetService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<DeleteAssetResponseDto>?> deleteAssets(
|
Future<List<DeleteAssetResponseDto>?> deleteAssets(
|
||||||
Set<AssetResponseDto> deleteAssets,
|
Iterable<AssetResponseDto> deleteAssets,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
List<String> payload = [];
|
final List<String> payload = [];
|
||||||
|
|
||||||
for (var asset in deleteAssets) {
|
for (final asset in deleteAssets) {
|
||||||
payload.add(asset.id);
|
payload.add(asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,24 @@
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/services/json_cache.dart';
|
import 'package:immich_mobile/shared/services/json_cache.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
|
class AssetCacheService extends JsonCache<List<Asset>> {
|
||||||
class AssetCacheService extends JsonCache<List<AssetResponseDto>> {
|
|
||||||
AssetCacheService() : super("asset_cache");
|
AssetCacheService() : super("asset_cache");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void put(List<AssetResponseDto> data) {
|
void put(List<Asset> data) {
|
||||||
putRawData(data.map((e) => e.toJson()).toList());
|
putRawData(data.map((e) => e.toJson()).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<AssetResponseDto>> get() async {
|
Future<List<Asset>> get() async {
|
||||||
try {
|
try {
|
||||||
final mapList = await readRawData() as List<dynamic>;
|
final mapList = await readRawData() as List<dynamic>;
|
||||||
|
|
||||||
final responseData = mapList
|
final responseData =
|
||||||
.map((e) => AssetResponseDto.fromJson(e))
|
mapList.map((e) => Asset.fromJson(e)).whereNotNull().toList();
|
||||||
.whereNotNull()
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return responseData;
|
return responseData;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -33,5 +30,5 @@ class AssetCacheService extends JsonCache<List<AssetResponseDto>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetCacheServiceProvider = Provider(
|
final assetCacheServiceProvider = Provider(
|
||||||
(ref) => AssetCacheService(),
|
(ref) => AssetCacheService(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
enum RenderAssetGridElementType {
|
enum RenderAssetGridElementType {
|
||||||
assetRow,
|
assetRow,
|
||||||
|
@ -9,7 +9,7 @@ enum RenderAssetGridElementType {
|
||||||
}
|
}
|
||||||
|
|
||||||
class RenderAssetGridRow {
|
class RenderAssetGridRow {
|
||||||
final List<AssetResponseDto> assets;
|
final List<Asset> assets;
|
||||||
|
|
||||||
RenderAssetGridRow(this.assets);
|
RenderAssetGridRow(this.assets);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ class RenderAssetGridElement {
|
||||||
final RenderAssetGridRow? assetRow;
|
final RenderAssetGridRow? assetRow;
|
||||||
final String? title;
|
final String? title;
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
final List<AssetResponseDto>? relatedAssetList;
|
final List<Asset>? relatedAssetList;
|
||||||
|
|
||||||
RenderAssetGridElement(
|
RenderAssetGridElement(
|
||||||
this.type, {
|
this.type, {
|
||||||
|
@ -31,13 +31,15 @@ class RenderAssetGridElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<RenderAssetGridElement> assetsToRenderList(
|
List<RenderAssetGridElement> assetsToRenderList(
|
||||||
List<AssetResponseDto> assets, int assetsPerRow) {
|
List<Asset> assets,
|
||||||
|
int assetsPerRow,
|
||||||
|
) {
|
||||||
List<RenderAssetGridElement> elements = [];
|
List<RenderAssetGridElement> elements = [];
|
||||||
|
|
||||||
int cursor = 0;
|
int cursor = 0;
|
||||||
while (cursor < assets.length) {
|
while (cursor < assets.length) {
|
||||||
int rowElements = min(assets.length - cursor, assetsPerRow);
|
int rowElements = min(assets.length - cursor, assetsPerRow);
|
||||||
final date = DateTime.parse(assets[cursor].createdAt);
|
final date = assets[cursor].createdAt;
|
||||||
|
|
||||||
final rowElement = RenderAssetGridElement(
|
final rowElement = RenderAssetGridElement(
|
||||||
RenderAssetGridElementType.assetRow,
|
RenderAssetGridElementType.assetRow,
|
||||||
|
@ -55,7 +57,9 @@ List<RenderAssetGridElement> assetsToRenderList(
|
||||||
}
|
}
|
||||||
|
|
||||||
List<RenderAssetGridElement> assetGroupsToRenderList(
|
List<RenderAssetGridElement> assetGroupsToRenderList(
|
||||||
Map<String, List<AssetResponseDto>> assetGroups, int assetsPerRow) {
|
Map<String, List<Asset>> assetGroups,
|
||||||
|
int assetsPerRow,
|
||||||
|
) {
|
||||||
List<RenderAssetGridElement> elements = [];
|
List<RenderAssetGridElement> elements = [];
|
||||||
DateTime? lastDate;
|
DateTime? lastDate;
|
||||||
|
|
||||||
|
@ -64,8 +68,11 @@ List<RenderAssetGridElement> assetGroupsToRenderList(
|
||||||
|
|
||||||
if (lastDate == null || lastDate!.month != date.month) {
|
if (lastDate == null || lastDate!.month != date.month) {
|
||||||
elements.add(
|
elements.add(
|
||||||
RenderAssetGridElement(RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElement(
|
||||||
title: groupName, date: date),
|
RenderAssetGridElementType.monthTitle,
|
||||||
|
title: groupName,
|
||||||
|
date: date,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'asset_grid_data_structure.dart';
|
import 'asset_grid_data_structure.dart';
|
||||||
import 'daily_title_text.dart';
|
import 'daily_title_text.dart';
|
||||||
|
@ -13,7 +13,7 @@ import 'draggable_scrollbar_custom.dart';
|
||||||
|
|
||||||
typedef ImmichAssetGridSelectionListener = void Function(
|
typedef ImmichAssetGridSelectionListener = void Function(
|
||||||
bool,
|
bool,
|
||||||
Set<AssetResponseDto>,
|
Set<Asset>,
|
||||||
);
|
);
|
||||||
|
|
||||||
class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||||
|
@ -24,20 +24,20 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||||
bool _scrolling = false;
|
bool _scrolling = false;
|
||||||
final Set<String> _selectedAssets = HashSet();
|
final Set<String> _selectedAssets = HashSet();
|
||||||
|
|
||||||
List<AssetResponseDto> get _assets {
|
List<Asset> get _assets {
|
||||||
return widget.renderList
|
return widget.renderList
|
||||||
.map((e) {
|
.map((e) {
|
||||||
if (e.type == RenderAssetGridElementType.assetRow) {
|
if (e.type == RenderAssetGridElementType.assetRow) {
|
||||||
return e.assetRow!.assets;
|
return e.assetRow!.assets;
|
||||||
} else {
|
} else {
|
||||||
return List<AssetResponseDto>.empty();
|
return List<Asset>.empty();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.flattened
|
.flattened
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<AssetResponseDto> _getSelectedAssets() {
|
Set<Asset> _getSelectedAssets() {
|
||||||
return _selectedAssets
|
return _selectedAssets
|
||||||
.map((e) => _assets.firstWhereOrNull((a) => a.id == e))
|
.map((e) => _assets.firstWhereOrNull((a) => a.id == e))
|
||||||
.whereNotNull()
|
.whereNotNull()
|
||||||
|
@ -48,7 +48,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||||
widget.listener?.call(selectionActive, _getSelectedAssets());
|
widget.listener?.call(selectionActive, _getSelectedAssets());
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectAssets(List<AssetResponseDto> assets) {
|
void _selectAssets(List<Asset> assets) {
|
||||||
setState(() {
|
setState(() {
|
||||||
for (var e in assets) {
|
for (var e in assets) {
|
||||||
_selectedAssets.add(e.id);
|
_selectedAssets.add(e.id);
|
||||||
|
@ -57,7 +57,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deselectAssets(List<AssetResponseDto> assets) {
|
void _deselectAssets(List<Asset> assets) {
|
||||||
setState(() {
|
setState(() {
|
||||||
for (var e in assets) {
|
for (var e in assets) {
|
||||||
_selectedAssets.remove(e.id);
|
_selectedAssets.remove(e.id);
|
||||||
|
@ -74,7 +74,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||||
_callSelectionListener(false);
|
_callSelectionListener(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _allAssetsSelected(List<AssetResponseDto> assets) {
|
bool _allAssetsSelected(List<Asset> assets) {
|
||||||
return widget.selectionActive &&
|
return widget.selectionActive &&
|
||||||
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
|
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildThumbnailOrPlaceholder(
|
Widget _buildThumbnailOrPlaceholder(
|
||||||
AssetResponseDto asset,
|
Asset asset,
|
||||||
bool placeholder,
|
bool placeholder,
|
||||||
) {
|
) {
|
||||||
if (placeholder) {
|
if (placeholder) {
|
||||||
|
@ -114,7 +114,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
key: Key("asset-row-${row.assets.first.id}"),
|
key: Key("asset-row-${row.assets.first.id}"),
|
||||||
children: row.assets.map((AssetResponseDto asset) {
|
children: row.assets.map((Asset asset) {
|
||||||
bool last = asset == row.assets.last;
|
bool last = asset == row.assets.last;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
@ -134,7 +134,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||||
Widget _buildTitle(
|
Widget _buildTitle(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
String title,
|
String title,
|
||||||
List<AssetResponseDto> assets,
|
List<Asset> assets,
|
||||||
) {
|
) {
|
||||||
return DailyTitleText(
|
return DailyTitleText(
|
||||||
isoDate: title,
|
isoDate: title,
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
|
|
||||||
class ThumbnailImage extends HookConsumerWidget {
|
class ThumbnailImage extends HookConsumerWidget {
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
final List<AssetResponseDto> assetList;
|
final List<Asset> assetList;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
final bool useGrayBoxPlaceholder;
|
final bool useGrayBoxPlaceholder;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
|
@ -34,12 +31,9 @@ class ThumbnailImage extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
|
||||||
var thumbnailRequestUrl = getThumbnailUrl(asset);
|
|
||||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||||
|
|
||||||
|
Widget buildSelectionIcon(Asset asset) {
|
||||||
Widget buildSelectionIcon(AssetResponseDto asset) {
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
|
@ -87,41 +81,11 @@ class ThumbnailImage extends HookConsumerWidget {
|
||||||
)
|
)
|
||||||
: const Border(),
|
: const Border(),
|
||||||
),
|
),
|
||||||
child: CachedNetworkImage(
|
child: ImmichImage(
|
||||||
cacheKey: 'thumbnail-image-${asset.id}',
|
asset,
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 300,
|
height: 300,
|
||||||
memCacheHeight: 200,
|
useGrayBoxPlaceholder: useGrayBoxPlaceholder,
|
||||||
maxWidthDiskCache: 200,
|
|
||||||
maxHeightDiskCache: 200,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
imageUrl: thumbnailRequestUrl,
|
|
||||||
httpHeaders: {
|
|
||||||
"Authorization": "Bearer ${box.get(accessTokenKey)}"
|
|
||||||
},
|
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) {
|
|
||||||
if (useGrayBoxPlaceholder) {
|
|
||||||
return const DecoratedBox(
|
|
||||||
decoration: BoxDecoration(color: Colors.grey),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Transform.scale(
|
|
||||||
scale: 0.2,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
value: downloadProgress.progress,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
errorWidget: (context, url, error) {
|
|
||||||
debugPrint("Error getting thumbnail $url = $error");
|
|
||||||
CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
|
|
||||||
|
|
||||||
return Icon(
|
|
||||||
Icons.image_not_supported_outlined,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (multiselectEnabled)
|
if (multiselectEnabled)
|
||||||
|
@ -137,14 +101,16 @@ class ThumbnailImage extends HookConsumerWidget {
|
||||||
right: 10,
|
right: 10,
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
(deviceId != asset.deviceId)
|
asset.isRemote
|
||||||
? Icons.cloud_done_outlined
|
? (deviceId == asset.deviceId
|
||||||
: Icons.photo_library_rounded,
|
? Icons.cloud_done_outlined
|
||||||
|
: Icons.cloud_outlined)
|
||||||
|
: Icons.cloud_off_outlined,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (asset.type != AssetTypeEnum.IMAGE)
|
if (!asset.isImage)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 5,
|
top: 5,
|
||||||
right: 5,
|
right: 5,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
|
@ -14,6 +15,7 @@ import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
|
@ -31,7 +33,7 @@ class HomePage extends HookConsumerWidget {
|
||||||
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
||||||
final selectionEnabledHook = useState(false);
|
final selectionEnabledHook = useState(false);
|
||||||
|
|
||||||
final selection = useState(<AssetResponseDto>{});
|
final selection = useState(<Asset>{});
|
||||||
final albums = ref.watch(albumProvider);
|
final albums = ref.watch(albumProvider);
|
||||||
final albumService = ref.watch(albumServiceProvider);
|
final albumService = ref.watch(albumServiceProvider);
|
||||||
|
|
||||||
|
@ -60,7 +62,7 @@ class HomePage extends HookConsumerWidget {
|
||||||
Widget buildBody() {
|
Widget buildBody() {
|
||||||
void selectionListener(
|
void selectionListener(
|
||||||
bool multiselect,
|
bool multiselect,
|
||||||
Set<AssetResponseDto> selectedAssets,
|
Set<Asset> selectedAssets,
|
||||||
) {
|
) {
|
||||||
selectionEnabledHook.value = multiselect;
|
selectionEnabledHook.value = multiselect;
|
||||||
selection.value = selectedAssets;
|
selection.value = selectedAssets;
|
||||||
|
@ -76,9 +78,27 @@ class HomePage extends HookConsumerWidget {
|
||||||
selectionEnabledHook.value = false;
|
selectionEnabledHook.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Iterable<Asset> remoteOnlySelection() {
|
||||||
|
final Set<Asset> assets = selection.value;
|
||||||
|
final bool onlyRemote = assets.every((e) => e.isRemote);
|
||||||
|
if (!onlyRemote) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "Can not add local assets to albums yet, skipping",
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
return assets.where((a) => a.isRemote);
|
||||||
|
}
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
void onAddToAlbum(AlbumResponseDto album) async {
|
void onAddToAlbum(AlbumResponseDto album) async {
|
||||||
|
final Iterable<Asset> assets = remoteOnlySelection();
|
||||||
|
if (assets.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final result = await albumService.addAdditionalAssetToAlbum(
|
final result = await albumService.addAdditionalAssetToAlbum(
|
||||||
selection.value,
|
assets,
|
||||||
album.id,
|
album.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -103,6 +123,7 @@ class HomePage extends HookConsumerWidget {
|
||||||
"added": result.successfullyAdded.toString(),
|
"added": result.successfullyAdded.toString(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
toastType: ToastType.success,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,8 +132,11 @@ class HomePage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
void onCreateNewAlbum() async {
|
void onCreateNewAlbum() async {
|
||||||
final result =
|
final Iterable<Asset> assets = remoteOnlySelection();
|
||||||
await albumService.createAlbumWithGeneratedName(selection.value);
|
if (assets.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final result = await albumService.createAlbumWithGeneratedName(assets);
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SearchResultPageState {
|
class SearchResultPageState {
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final bool isSuccess;
|
final bool isSuccess;
|
||||||
final bool isError;
|
final bool isError;
|
||||||
final List<AssetResponseDto> searchResult;
|
final List<Asset> searchResult;
|
||||||
|
|
||||||
SearchResultPageState({
|
SearchResultPageState({
|
||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
|
@ -20,7 +21,7 @@ class SearchResultPageState {
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
bool? isSuccess,
|
bool? isSuccess,
|
||||||
bool? isError,
|
bool? isError,
|
||||||
List<AssetResponseDto>? searchResult,
|
List<Asset>? searchResult,
|
||||||
}) {
|
}) {
|
||||||
return SearchResultPageState(
|
return SearchResultPageState(
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
@ -44,8 +45,9 @@ class SearchResultPageState {
|
||||||
isLoading: map['isLoading'] ?? false,
|
isLoading: map['isLoading'] ?? false,
|
||||||
isSuccess: map['isSuccess'] ?? false,
|
isSuccess: map['isSuccess'] ?? false,
|
||||||
isError: map['isError'] ?? false,
|
isError: map['isError'] ?? false,
|
||||||
searchResult: List<AssetResponseDto>.from(
|
searchResult: List<Asset>.from(
|
||||||
map['searchResult']?.map((x) => AssetResponseDto.mapFromJson(x)),
|
map['searchResult']
|
||||||
|
?.map((x) => Asset.remote(AssetResponseDto.fromJson(x))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@ import 'package:immich_mobile/modules/search/models/search_result_page_state.mod
|
||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||||
SearchResultPageNotifier(this._searchService)
|
SearchResultPageNotifier(this._searchService)
|
||||||
|
@ -30,8 +30,9 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||||
isSuccess: false,
|
isSuccess: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
List<AssetResponseDto>? assets =
|
List<Asset>? assets = (await _searchService.searchAsset(searchTerm))
|
||||||
await _searchService.searchAsset(searchTerm);
|
?.map((e) => Asset.remote(e))
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (assets != null) {
|
if (assets != null) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
|
@ -61,12 +62,11 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) {
|
||||||
var assets = ref.watch(searchResultPageProvider).searchResult;
|
var assets = ref.watch(searchResultPageProvider).searchResult;
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>(
|
assets.sortByCompare<DateTime>(
|
||||||
(e) => DateTime.parse(e.createdAt),
|
(e) => e.createdAt,
|
||||||
(a, b) => b.compareTo(a),
|
(a, b) => b.compareTo(a),
|
||||||
);
|
);
|
||||||
return assets.groupListsBy(
|
return assets.groupListsBy(
|
||||||
(element) => DateFormat('y-MM-dd')
|
(element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()),
|
||||||
.format(DateTime.parse(element.createdAt).toLocal()),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ import 'package:immich_mobile/modules/settings/views/settings_page.dart';
|
||||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||||
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
||||||
|
|
|
@ -65,8 +65,7 @@ class _$AppRouter extends RootStackRouter {
|
||||||
final args = routeData.argsAs<VideoViewerRouteArgs>();
|
final args = routeData.argsAs<VideoViewerRouteArgs>();
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: VideoViewerPage(
|
child: VideoViewerPage(key: args.key, asset: args.asset));
|
||||||
key: args.key, videoUrl: args.videoUrl, asset: args.asset));
|
|
||||||
},
|
},
|
||||||
BackupControllerRoute.name: (routeData) {
|
BackupControllerRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
|
@ -258,9 +257,7 @@ class TabControllerRoute extends PageRouteInfo<void> {
|
||||||
/// [GalleryViewerPage]
|
/// [GalleryViewerPage]
|
||||||
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
||||||
GalleryViewerRoute(
|
GalleryViewerRoute(
|
||||||
{Key? key,
|
{Key? key, required List<Asset> assetList, required Asset asset})
|
||||||
required List<AssetResponseDto> assetList,
|
|
||||||
required AssetResponseDto asset})
|
|
||||||
: super(GalleryViewerRoute.name,
|
: super(GalleryViewerRoute.name,
|
||||||
path: '/gallery-viewer-page',
|
path: '/gallery-viewer-page',
|
||||||
args: GalleryViewerRouteArgs(
|
args: GalleryViewerRouteArgs(
|
||||||
|
@ -275,9 +272,9 @@ class GalleryViewerRouteArgs {
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final List<AssetResponseDto> assetList;
|
final List<Asset> assetList;
|
||||||
|
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
@ -291,7 +288,7 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
|
||||||
ImageViewerRoute(
|
ImageViewerRoute(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
required String heroTag,
|
required String heroTag,
|
||||||
required AssetResponseDto asset,
|
required Asset asset,
|
||||||
required String authToken,
|
required String authToken,
|
||||||
required void Function() isZoomedFunction,
|
required void Function() isZoomedFunction,
|
||||||
required ValueNotifier<bool> isZoomedListener,
|
required ValueNotifier<bool> isZoomedListener,
|
||||||
|
@ -324,7 +321,7 @@ class ImageViewerRouteArgs {
|
||||||
|
|
||||||
final String heroTag;
|
final String heroTag;
|
||||||
|
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
|
|
||||||
final String authToken;
|
final String authToken;
|
||||||
|
|
||||||
|
@ -343,29 +340,24 @@ class ImageViewerRouteArgs {
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [VideoViewerPage]
|
/// [VideoViewerPage]
|
||||||
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
||||||
VideoViewerRoute(
|
VideoViewerRoute({Key? key, required Asset asset})
|
||||||
{Key? key, required String videoUrl, required AssetResponseDto asset})
|
|
||||||
: super(VideoViewerRoute.name,
|
: super(VideoViewerRoute.name,
|
||||||
path: '/video-viewer-page',
|
path: '/video-viewer-page',
|
||||||
args: VideoViewerRouteArgs(
|
args: VideoViewerRouteArgs(key: key, asset: asset));
|
||||||
key: key, videoUrl: videoUrl, asset: asset));
|
|
||||||
|
|
||||||
static const String name = 'VideoViewerRoute';
|
static const String name = 'VideoViewerRoute';
|
||||||
}
|
}
|
||||||
|
|
||||||
class VideoViewerRouteArgs {
|
class VideoViewerRouteArgs {
|
||||||
const VideoViewerRouteArgs(
|
const VideoViewerRouteArgs({this.key, required this.asset});
|
||||||
{this.key, required this.videoUrl, required this.asset});
|
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final String videoUrl;
|
final Asset asset;
|
||||||
|
|
||||||
final AssetResponseDto asset;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl, asset: $asset}';
|
return 'VideoViewerRouteArgs{key: $key, asset: $asset}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
117
mobile/lib/shared/models/asset.dart
Normal file
117
mobile/lib/shared/models/asset.dart
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
/// Asset (online or local)
|
||||||
|
class Asset {
|
||||||
|
Asset.remote(this.remote) {
|
||||||
|
local = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Asset.local(this.local) {
|
||||||
|
remote = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
late final AssetResponseDto? remote;
|
||||||
|
late final AssetEntity? local;
|
||||||
|
|
||||||
|
bool get isRemote => remote != null;
|
||||||
|
bool get isLocal => local != null;
|
||||||
|
|
||||||
|
String get deviceId =>
|
||||||
|
isRemote ? remote!.deviceId : Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
|
|
||||||
|
String get deviceAssetId => isRemote ? remote!.deviceAssetId : local!.id;
|
||||||
|
|
||||||
|
String get id => isLocal ? local!.id : remote!.id;
|
||||||
|
|
||||||
|
double? get latitude =>
|
||||||
|
isLocal ? local!.latitude : remote!.exifInfo?.latitude?.toDouble();
|
||||||
|
|
||||||
|
double? get longitude =>
|
||||||
|
isLocal ? local!.longitude : remote!.exifInfo?.longitude?.toDouble();
|
||||||
|
|
||||||
|
DateTime get createdAt =>
|
||||||
|
isLocal ? local!.createDateTime : DateTime.parse(remote!.createdAt);
|
||||||
|
|
||||||
|
bool get isImage => isLocal
|
||||||
|
? local!.type == AssetType.image
|
||||||
|
: remote!.type == AssetTypeEnum.IMAGE;
|
||||||
|
|
||||||
|
String get duration => isRemote
|
||||||
|
? remote!.duration
|
||||||
|
: Duration(seconds: local!.duration).toString();
|
||||||
|
|
||||||
|
/// use only for tests
|
||||||
|
set createdAt(DateTime val) {
|
||||||
|
if (isRemote) {
|
||||||
|
remote!.createdAt = val.toIso8601String();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
if (isLocal) {
|
||||||
|
json["local"] = _assetEntityToJson(local!);
|
||||||
|
} else {
|
||||||
|
json["remote"] = remote!.toJson();
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Asset? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
final l = json["local"];
|
||||||
|
if (l != null) {
|
||||||
|
return Asset.local(_assetEntityFromJson(l));
|
||||||
|
} else {
|
||||||
|
return Asset.remote(AssetResponseDto.fromJson(json["remote"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _assetEntityToJson(AssetEntity a) {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json["id"] = a.id;
|
||||||
|
json["typeInt"] = a.typeInt;
|
||||||
|
json["width"] = a.width;
|
||||||
|
json["height"] = a.height;
|
||||||
|
json["duration"] = a.duration;
|
||||||
|
json["orientation"] = a.orientation;
|
||||||
|
json["isFavorite"] = a.isFavorite;
|
||||||
|
json["title"] = a.title;
|
||||||
|
json["createDateSecond"] = a.createDateSecond;
|
||||||
|
json["modifiedDateSecond"] = a.modifiedDateSecond;
|
||||||
|
json["latitude"] = a.latitude;
|
||||||
|
json["longitude"] = a.longitude;
|
||||||
|
json["mimeType"] = a.mimeType;
|
||||||
|
json["subtype"] = a.subtype;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetEntity? _assetEntityFromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
return AssetEntity(
|
||||||
|
id: json["id"],
|
||||||
|
typeInt: json["typeInt"],
|
||||||
|
width: json["width"],
|
||||||
|
height: json["height"],
|
||||||
|
duration: json["duration"],
|
||||||
|
orientation: json["orientation"],
|
||||||
|
isFavorite: json["isFavorite"],
|
||||||
|
title: json["title"],
|
||||||
|
createDateSecond: json["createDateSecond"],
|
||||||
|
modifiedDateSecond: json["modifiedDateSecond"],
|
||||||
|
latitude: json["latitude"],
|
||||||
|
longitude: json["longitude"],
|
||||||
|
mimeType: json["mimeType"],
|
||||||
|
subtype: json["subtype"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -1,18 +1,23 @@
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
|
class AssetNotifier extends StateNotifier<List<Asset>> {
|
||||||
final AssetService _assetService;
|
final AssetService _assetService;
|
||||||
final AssetCacheService _assetCacheService;
|
final AssetCacheService _assetCacheService;
|
||||||
|
|
||||||
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
||||||
|
bool _getAllAssetInProgress = false;
|
||||||
|
bool _deleteInProgress = false;
|
||||||
|
|
||||||
AssetNotifier(this._assetService, this._assetCacheService) : super([]);
|
AssetNotifier(this._assetService, this._assetCacheService) : super([]);
|
||||||
|
|
||||||
|
@ -21,29 +26,38 @@ class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllAsset() async {
|
getAllAsset() async {
|
||||||
final stopwatch = Stopwatch();
|
if (_getAllAssetInProgress || _deleteInProgress) {
|
||||||
|
// guard against multiple calls to this method while it's still working
|
||||||
|
return;
|
||||||
if (await _assetCacheService.isValid() && state.isEmpty) {
|
|
||||||
stopwatch.start();
|
|
||||||
state = await _assetCacheService.get();
|
|
||||||
debugPrint("Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms");
|
|
||||||
stopwatch.reset();
|
|
||||||
}
|
}
|
||||||
|
final stopwatch = Stopwatch();
|
||||||
|
try {
|
||||||
|
_getAllAssetInProgress = true;
|
||||||
|
|
||||||
|
final bool isCacheValid = await _assetCacheService.isValid();
|
||||||
|
if (isCacheValid && state.isEmpty) {
|
||||||
|
stopwatch.start();
|
||||||
|
state = await _assetCacheService.get();
|
||||||
|
debugPrint(
|
||||||
|
"Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
|
stopwatch.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopwatch.start();
|
||||||
|
var allAssets = await _assetService.getAllAsset(urgent: !isCacheValid);
|
||||||
|
debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
|
stopwatch.reset();
|
||||||
|
|
||||||
|
state = allAssets;
|
||||||
|
} finally {
|
||||||
|
_getAllAssetInProgress = false;
|
||||||
|
}
|
||||||
|
debugPrint("[getAllAsset] setting new asset state");
|
||||||
|
|
||||||
stopwatch.start();
|
stopwatch.start();
|
||||||
var allAssets = await _assetService.getAllAsset();
|
_cacheState();
|
||||||
debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms");
|
debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
stopwatch.reset();
|
stopwatch.reset();
|
||||||
|
|
||||||
if (allAssets != null) {
|
|
||||||
state = allAssets;
|
|
||||||
|
|
||||||
stopwatch.start();
|
|
||||||
_cacheState();
|
|
||||||
debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
|
|
||||||
stopwatch.reset();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAllAsset() {
|
clearAllAsset() {
|
||||||
|
@ -52,80 +66,113 @@ class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewAssetUploaded(AssetResponseDto newAsset) {
|
onNewAssetUploaded(AssetResponseDto newAsset) {
|
||||||
state = [...state, newAsset];
|
final int i = state.indexWhere(
|
||||||
|
(a) =>
|
||||||
|
a.isRemote ||
|
||||||
|
(a.id == newAsset.deviceAssetId && a.deviceId == newAsset.deviceId),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (i == -1 || state[i].deviceAssetId != newAsset.deviceAssetId) {
|
||||||
|
state = [...state, Asset.remote(newAsset)];
|
||||||
|
} else {
|
||||||
|
// order is important to keep all local-only assets at the beginning!
|
||||||
|
state = [
|
||||||
|
...state.slice(0, i),
|
||||||
|
...state.slice(i + 1),
|
||||||
|
Asset.remote(newAsset),
|
||||||
|
];
|
||||||
|
// TODO here is a place to unify local/remote assets by replacing the
|
||||||
|
// local-only asset in the state with a local&remote asset
|
||||||
|
}
|
||||||
_cacheState();
|
_cacheState();
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAssets(Set<AssetResponseDto> deleteAssets) async {
|
deleteAssets(Set<Asset> deleteAssets) async {
|
||||||
|
_deleteInProgress = true;
|
||||||
|
try {
|
||||||
|
final localDeleted = await _deleteLocalAssets(deleteAssets);
|
||||||
|
final remoteDeleted = await _deleteRemoteAssets(deleteAssets);
|
||||||
|
final Set<String> deleted = HashSet();
|
||||||
|
deleted.addAll(localDeleted);
|
||||||
|
deleted.addAll(remoteDeleted);
|
||||||
|
if (deleted.isNotEmpty) {
|
||||||
|
state = state.where((a) => !deleted.contains(a.id)).toList();
|
||||||
|
_cacheState();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_deleteInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> _deleteLocalAssets(Set<Asset> assetsToDelete) async {
|
||||||
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
||||||
var deviceId = deviceInfo["deviceId"];
|
var deviceId = deviceInfo["deviceId"];
|
||||||
var deleteIdList = <String>[];
|
final List<String> local = [];
|
||||||
// Delete asset from device
|
// Delete asset from device
|
||||||
for (var asset in deleteAssets) {
|
for (final Asset asset in assetsToDelete) {
|
||||||
// Delete asset on device if present
|
if (asset.isLocal) {
|
||||||
if (asset.deviceId == deviceId) {
|
local.add(asset.id);
|
||||||
|
} else if (asset.deviceId == deviceId) {
|
||||||
|
// Delete asset on device if it is still present
|
||||||
var localAsset = await AssetEntity.fromId(asset.deviceAssetId);
|
var localAsset = await AssetEntity.fromId(asset.deviceAssetId);
|
||||||
|
|
||||||
if (localAsset != null) {
|
if (localAsset != null) {
|
||||||
deleteIdList.add(localAsset.id);
|
local.add(localAsset.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (local.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
await PhotoManager.editor.deleteWithIds(deleteIdList);
|
return await PhotoManager.editor.deleteWithIds(local);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Delete asset from device failed: $e");
|
debugPrint("Delete asset from device failed: $e");
|
||||||
}
|
|
||||||
|
|
||||||
// Delete asset on server
|
|
||||||
List<DeleteAssetResponseDto>? deleteAssetResult =
|
|
||||||
await _assetService.deleteAssets(deleteAssets);
|
|
||||||
|
|
||||||
if (deleteAssetResult == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var asset in deleteAssetResult) {
|
|
||||||
if (asset.status == DeleteAssetStatus.SUCCESS) {
|
|
||||||
state =
|
|
||||||
state.where((immichAsset) => immichAsset.id != asset.id).toList();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
_cacheState();
|
Future<Iterable<String>> _deleteRemoteAssets(
|
||||||
|
Set<Asset> assetsToDelete,
|
||||||
|
) async {
|
||||||
|
final Iterable<AssetResponseDto> remote =
|
||||||
|
assetsToDelete.where((e) => e.isRemote).map((e) => e.remote!);
|
||||||
|
final List<DeleteAssetResponseDto> deleteAssetResult =
|
||||||
|
await _assetService.deleteAssets(remote) ?? [];
|
||||||
|
return deleteAssetResult
|
||||||
|
.where((a) => a.status == DeleteAssetStatus.SUCCESS)
|
||||||
|
.map((a) => a.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetProvider =
|
final assetProvider = StateNotifierProvider<AssetNotifier, List<Asset>>((ref) {
|
||||||
StateNotifierProvider<AssetNotifier, List<AssetResponseDto>>((ref) {
|
|
||||||
return AssetNotifier(
|
return AssetNotifier(
|
||||||
ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider));
|
ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
final assetGroupByDateTimeProvider = StateProvider((ref) {
|
final assetGroupByDateTimeProvider = StateProvider((ref) {
|
||||||
var assets = ref.watch(assetProvider);
|
final assets = ref.watch(assetProvider).toList();
|
||||||
|
// `toList()` ist needed to make a copy as to NOT sort the original list/state
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>(
|
assets.sortByCompare<DateTime>(
|
||||||
(e) => DateTime.parse(e.createdAt),
|
(e) => e.createdAt,
|
||||||
(a, b) => b.compareTo(a),
|
(a, b) => b.compareTo(a),
|
||||||
);
|
);
|
||||||
return assets.groupListsBy(
|
return assets.groupListsBy(
|
||||||
(element) => DateFormat('y-MM-dd')
|
(element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()),
|
||||||
.format(DateTime.parse(element.createdAt).toLocal()),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
final assetGroupByMonthYearProvider = StateProvider((ref) {
|
final assetGroupByMonthYearProvider = StateProvider((ref) {
|
||||||
var assets = ref.watch(assetProvider);
|
// TODO: remove `where` once temporary workaround is no longer needed (to only
|
||||||
|
// allow remote assets to be added to album). Keep `toList()` as to NOT sort
|
||||||
|
// the original list/state
|
||||||
|
final assets = ref.watch(assetProvider).where((e) => e.isRemote).toList();
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>(
|
assets.sortByCompare<DateTime>(
|
||||||
(e) => DateTime.parse(e.createdAt),
|
(e) => e.createdAt,
|
||||||
(a, b) => b.compareTo(a),
|
(a, b) => b.compareTo(a),
|
||||||
);
|
);
|
||||||
|
|
||||||
return assets.groupListsBy(
|
return assets.groupListsBy(
|
||||||
(element) => DateFormat('MMMM, y')
|
(element) => DateFormat('MMMM, y').format(element.createdAt.toLocal()),
|
||||||
.format(DateTime.parse(element.createdAt).toLocal()),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,11 +2,11 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'api.service.dart';
|
import 'api.service.dart';
|
||||||
|
|
||||||
final shareServiceProvider =
|
final shareServiceProvider =
|
||||||
|
@ -17,26 +17,28 @@ class ShareService {
|
||||||
|
|
||||||
ShareService(this._apiService);
|
ShareService(this._apiService);
|
||||||
|
|
||||||
Future<void> shareAsset(AssetResponseDto asset) async {
|
Future<void> shareAsset(Asset asset) async {
|
||||||
await shareAssets([asset]);
|
await shareAssets([asset]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> shareAssets(List<AssetResponseDto> assets) async {
|
Future<void> shareAssets(List<Asset> assets) async {
|
||||||
final downloadedFilePaths = assets.map((asset) async {
|
final downloadedFilePaths = assets.map((asset) async {
|
||||||
final res = await _apiService.assetApi.downloadFileWithHttpInfo(
|
if (asset.isRemote) {
|
||||||
asset.deviceAssetId,
|
final tempDir = await getTemporaryDirectory();
|
||||||
asset.deviceId,
|
final fileName = basename(asset.remote!.originalPath);
|
||||||
isThumb: false,
|
final tempFile = await File('${tempDir.path}/$fileName').create();
|
||||||
isWeb: false,
|
final res = await _apiService.assetApi.downloadFileWithHttpInfo(
|
||||||
);
|
asset.remote!.deviceAssetId,
|
||||||
|
asset.remote!.deviceId,
|
||||||
final fileName = p.basename(asset.originalPath);
|
isThumb: false,
|
||||||
|
isWeb: false,
|
||||||
final tempDir = await getTemporaryDirectory();
|
);
|
||||||
final tempFile = await File('${tempDir.path}/$fileName').create();
|
tempFile.writeAsBytesSync(res.bodyBytes);
|
||||||
tempFile.writeAsBytesSync(res.bodyBytes);
|
return tempFile.path;
|
||||||
|
} else {
|
||||||
return tempFile.path;
|
File? f = await asset.local!.file;
|
||||||
|
return f!.path;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Share.shareFiles(
|
Share.shareFiles(
|
||||||
|
|
96
mobile/lib/shared/ui/immich_image.dart
Normal file
96
mobile/lib/shared/ui/immich_image.dart
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
/// Renders an Asset using local data if available, else remote data
|
||||||
|
class ImmichImage extends StatelessWidget {
|
||||||
|
const ImmichImage(
|
||||||
|
this.asset, {
|
||||||
|
required this.width,
|
||||||
|
required this.height,
|
||||||
|
this.useGrayBoxPlaceholder = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final Asset asset;
|
||||||
|
final bool useGrayBoxPlaceholder;
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (asset.isLocal) {
|
||||||
|
return Image(
|
||||||
|
image: AssetEntityImageProvider(
|
||||||
|
asset.local!,
|
||||||
|
isOriginal: false,
|
||||||
|
thumbnailSize: const ThumbnailSize.square(250), // like server thumbs
|
||||||
|
),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||||
|
if (wasSynchronouslyLoaded || frame != null) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return (useGrayBoxPlaceholder
|
||||||
|
? const SizedBox.square(
|
||||||
|
dimension: 250,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(color: Colors.grey),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Transform.scale(
|
||||||
|
scale: 0.2,
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
debugPrint("Error getting thumb for assetId=${asset.id}: $error");
|
||||||
|
return Icon(
|
||||||
|
Icons.image_not_supported_outlined,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final String token = Hive.box(userInfoBox).get(accessTokenKey);
|
||||||
|
final String thumbnailRequestUrl = getThumbnailUrl(asset.remote!);
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: thumbnailRequestUrl,
|
||||||
|
httpHeaders: {"Authorization": "Bearer $token"},
|
||||||
|
cacheKey: 'thumbnail-image-${asset.id}',
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
// keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and
|
||||||
|
// maxHeightDiskCache = null allows to simply store the webp thumbnail
|
||||||
|
// from the server and use it for all rendered thumbnail sizes
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: const Duration(milliseconds: 250),
|
||||||
|
progressIndicatorBuilder: (context, url, downloadProgress) {
|
||||||
|
if (useGrayBoxPlaceholder) {
|
||||||
|
return const DecoratedBox(
|
||||||
|
decoration: BoxDecoration(color: Colors.grey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Transform.scale(
|
||||||
|
scale: 0.2,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: downloadProgress.progress,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorWidget: (context, url, error) {
|
||||||
|
debugPrint("Error getting thumbnail $url = $error");
|
||||||
|
CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
|
||||||
|
return Icon(
|
||||||
|
Icons.image_not_supported_outlined,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final List<AssetResponseDto> testAssets = [];
|
final List<Asset> testAssets = [];
|
||||||
|
|
||||||
for (int i = 0; i < 150; i++) {
|
for (int i = 0; i < 150; i++) {
|
||||||
int month = i ~/ 31;
|
int month = i ~/ 31;
|
||||||
|
@ -11,39 +12,43 @@ void main() {
|
||||||
|
|
||||||
DateTime date = DateTime(2022, month, day);
|
DateTime date = DateTime(2022, month, day);
|
||||||
|
|
||||||
testAssets.add(AssetResponseDto(
|
testAssets.add(
|
||||||
type: AssetTypeEnum.IMAGE,
|
Asset.remote(
|
||||||
id: '$i',
|
AssetResponseDto(
|
||||||
deviceAssetId: '',
|
type: AssetTypeEnum.IMAGE,
|
||||||
ownerId: '',
|
id: '$i',
|
||||||
deviceId: '',
|
deviceAssetId: '',
|
||||||
originalPath: '',
|
ownerId: '',
|
||||||
resizePath: '',
|
deviceId: '',
|
||||||
createdAt: date.toIso8601String(),
|
originalPath: '',
|
||||||
modifiedAt: date.toIso8601String(),
|
resizePath: '',
|
||||||
isFavorite: false,
|
createdAt: date.toIso8601String(),
|
||||||
mimeType: 'image/jpeg',
|
modifiedAt: date.toIso8601String(),
|
||||||
duration: '',
|
isFavorite: false,
|
||||||
webpPath: '',
|
mimeType: 'image/jpeg',
|
||||||
encodedVideoPath: '',
|
duration: '',
|
||||||
));
|
webpPath: '',
|
||||||
|
encodedVideoPath: '',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, List<AssetResponseDto>> groups = {
|
final Map<String, List<Asset>> groups = {
|
||||||
'2022-01-05': testAssets.sublist(0, 5).map((e) {
|
'2022-01-05': testAssets.sublist(0, 5).map((e) {
|
||||||
e.createdAt = DateTime(2022, 1, 5).toIso8601String();
|
e.createdAt = DateTime(2022, 1, 5);
|
||||||
return e;
|
return e;
|
||||||
}).toList(),
|
}).toList(),
|
||||||
'2022-01-10': testAssets.sublist(5, 10).map((e) {
|
'2022-01-10': testAssets.sublist(5, 10).map((e) {
|
||||||
e.createdAt = DateTime(2022, 1, 10).toIso8601String();
|
e.createdAt = DateTime(2022, 1, 10);
|
||||||
return e;
|
return e;
|
||||||
}).toList(),
|
}).toList(),
|
||||||
'2022-02-17': testAssets.sublist(10, 15).map((e) {
|
'2022-02-17': testAssets.sublist(10, 15).map((e) {
|
||||||
e.createdAt = DateTime(2022, 2, 17).toIso8601String();
|
e.createdAt = DateTime(2022, 2, 17);
|
||||||
return e;
|
return e;
|
||||||
}).toList(),
|
}).toList(),
|
||||||
'2022-10-15': testAssets.sublist(15, 30).map((e) {
|
'2022-10-15': testAssets.sublist(15, 30).map((e) {
|
||||||
e.createdAt = DateTime(2022, 10, 15).toIso8601String();
|
e.createdAt = DateTime(2022, 10, 15);
|
||||||
return e;
|
return e;
|
||||||
}).toList()
|
}).toList()
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue