mirror of
https://github.com/immich-app/immich.git
synced 2024-11-24 08:52:28 +02:00
Merge branch 'main' into feat/notification-email-template
This commit is contained in:
commit
c110bc0b07
@ -98,6 +98,10 @@ SELECT * FROM "move_history";
|
||||
SELECT * FROM "users";
|
||||
```
|
||||
|
||||
```sql title="Get owner info from asset ID"
|
||||
SELECT "users".* FROM "users" JOIN "assets" ON "users"."id" = "assets"."ownerId" WHERE "assets"."id" = 'fa310b01-2f26-4b7a-9042-d578226e021f';
|
||||
```
|
||||
|
||||
## System Config
|
||||
|
||||
```sql title="Custom settings"
|
||||
|
@ -163,11 +163,15 @@ describe('/server', () => {
|
||||
expect(body).toEqual({
|
||||
photos: 0,
|
||||
usage: 0,
|
||||
usagePhotos: 0,
|
||||
usageVideos: 0,
|
||||
usageByUser: [
|
||||
{
|
||||
quotaSizeInBytes: null,
|
||||
photos: 0,
|
||||
usage: 0,
|
||||
usagePhotos: 0,
|
||||
usageVideos: 0,
|
||||
userName: 'Immich Admin',
|
||||
userId: admin.userId,
|
||||
videos: 0,
|
||||
@ -176,6 +180,8 @@ describe('/server', () => {
|
||||
quotaSizeInBytes: null,
|
||||
photos: 0,
|
||||
usage: 0,
|
||||
usagePhotos: 0,
|
||||
usageVideos: 0,
|
||||
userName: 'User 1',
|
||||
userId: nonAdmin.userId,
|
||||
videos: 0,
|
||||
|
@ -21,6 +21,7 @@ const Color immichBrandColorLight = Color(0xFF4150AF);
|
||||
const Color immichBrandColorDark = Color(0xFFACCBFA);
|
||||
const Color whiteOpacity75 = Color.fromARGB((0.75 * 255) ~/ 1, 255, 255, 255);
|
||||
const Color blackOpacity90 = Color.fromARGB((0.90 * 255) ~/ 1, 0, 0, 0);
|
||||
const Color red400 = Color(0xFFEF5350);
|
||||
|
||||
final Map<ImmichColorPreset, ImmichTheme> _themePresetsMap = {
|
||||
ImmichColorPreset.indigo: ImmichTheme(
|
||||
|
@ -18,6 +18,9 @@ class CurrentUploadAsset {
|
||||
this.iCloudAsset,
|
||||
});
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
bool get isIcloudAsset => iCloudAsset != null && iCloudAsset!;
|
||||
|
||||
CurrentUploadAsset copyWith({
|
||||
String? id,
|
||||
DateTime? fileCreatedAt,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
|
||||
import 'package:immich_mobile/services/partner.service.dart';
|
||||
@ -9,9 +10,19 @@ import 'package:isar/isar.dart';
|
||||
|
||||
class PartnerSharedWithNotifier extends StateNotifier<List<User>> {
|
||||
PartnerSharedWithNotifier(Isar db, this._ps) : super([]) {
|
||||
final query = db.users.filter().isPartnerSharedWithEqualTo(true);
|
||||
query.findAll().then((partners) => state = partners);
|
||||
query.watch().listen((partners) => state = partners);
|
||||
Function eq = const ListEquality<User>().equals;
|
||||
final query = db.users.filter().isPartnerSharedWithEqualTo(true).sortById();
|
||||
query.findAll().then((partners) {
|
||||
if (!eq(state, partners)) {
|
||||
state = partners;
|
||||
}
|
||||
}).then((_) {
|
||||
query.watch().listen((partners) {
|
||||
if (!eq(state, partners)) {
|
||||
state = partners;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> updatePartner(User partner, {required bool inTimeline}) {
|
||||
@ -31,9 +42,19 @@ final partnerSharedWithProvider =
|
||||
|
||||
class PartnerSharedByNotifier extends StateNotifier<List<User>> {
|
||||
PartnerSharedByNotifier(Isar db) : super([]) {
|
||||
final query = db.users.filter().isPartnerSharedByEqualTo(true);
|
||||
query.findAll().then((partners) => state = partners);
|
||||
streamSub = query.watch().listen((partners) => state = partners);
|
||||
Function eq = const ListEquality<User>().equals;
|
||||
final query = db.users.filter().isPartnerSharedByEqualTo(true).sortById();
|
||||
query.findAll().then((partners) {
|
||||
if (!eq(state, partners)) {
|
||||
state = partners;
|
||||
}
|
||||
}).then((_) {
|
||||
streamSub = query.watch().listen((partners) {
|
||||
if (!eq(state, partners)) {
|
||||
state = partners;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
late final StreamSubscription<List<User>> streamSub;
|
||||
|
102
mobile/lib/widgets/backup/asset_info_table.dart
Normal file
102
mobile/lib/widgets/backup/asset_info_table.dart
Normal file
@ -0,0 +1,102 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
|
||||
class BackupAssetInfoTable extends ConsumerWidget {
|
||||
const BackupAssetInfoTable({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isManualUpload = ref.watch(
|
||||
backupProvider.select(
|
||||
(value) => value.backupProgress == BackUpProgressEnum.manualInProgress,
|
||||
),
|
||||
);
|
||||
|
||||
final asset = isManualUpload
|
||||
? ref.watch(
|
||||
manualUploadProvider.select((value) => value.currentUploadAsset),
|
||||
)
|
||||
: ref.watch(backupProvider.select((value) => value.currentUploadAsset));
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Table(
|
||||
border: TableBorder.all(
|
||||
color: context.colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
children: [
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text(
|
||||
'backup_controller_page_filename',
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
).tr(
|
||||
args: [asset.fileName, asset.fileType.toLowerCase()],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text(
|
||||
"backup_controller_page_created",
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
).tr(
|
||||
args: [_getAssetCreationDate(asset)],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text(
|
||||
"backup_controller_page_id",
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
).tr(args: [asset.id]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
String _getAssetCreationDate(CurrentUploadAsset asset) {
|
||||
return DateFormat.yMMMMd().format(asset.fileCreatedAt.toLocal());
|
||||
}
|
||||
}
|
@ -1,296 +1,43 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/widgets/backup/asset_info_table.dart';
|
||||
import 'package:immich_mobile/widgets/backup/error_chip.dart';
|
||||
import 'package:immich_mobile/widgets/backup/icloud_download_progress_bar.dart';
|
||||
import 'package:immich_mobile/widgets/backup/upload_progress_bar.dart';
|
||||
import 'package:immich_mobile/widgets/backup/upload_stats.dart';
|
||||
|
||||
class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
||||
class CurrentUploadingAssetInfoBox extends StatelessWidget {
|
||||
const CurrentUploadingAssetInfoBox({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
var isManualUpload = ref.watch(backupProvider).backupProgress ==
|
||||
BackUpProgressEnum.manualInProgress;
|
||||
var asset = !isManualUpload
|
||||
? ref.watch(backupProvider).currentUploadAsset
|
||||
: ref.watch(manualUploadProvider).currentUploadAsset;
|
||||
var uploadProgress = !isManualUpload
|
||||
? ref.watch(backupProvider).progressInPercentage
|
||||
: ref.watch(manualUploadProvider).progressInPercentage;
|
||||
var uploadFileProgress = !isManualUpload
|
||||
? ref.watch(backupProvider).progressInFileSize
|
||||
: ref.watch(manualUploadProvider).progressInFileSize;
|
||||
var uploadFileSpeed = !isManualUpload
|
||||
? ref.watch(backupProvider).progressInFileSpeed
|
||||
: ref.watch(manualUploadProvider).progressInFileSpeed;
|
||||
var iCloudDownloadProgress =
|
||||
ref.watch(backupProvider).iCloudDownloadProgress;
|
||||
final isShowThumbnail = useState(false);
|
||||
|
||||
String formatUploadFileSpeed(double uploadFileSpeed) {
|
||||
if (uploadFileSpeed < 1024) {
|
||||
return '${uploadFileSpeed.toStringAsFixed(2)} B/s';
|
||||
} else if (uploadFileSpeed < 1024 * 1024) {
|
||||
return '${(uploadFileSpeed / 1024).toStringAsFixed(2)} KB/s';
|
||||
} else if (uploadFileSpeed < 1024 * 1024 * 1024) {
|
||||
return '${(uploadFileSpeed / (1024 * 1024)).toStringAsFixed(2)} MB/s';
|
||||
} else {
|
||||
return '${(uploadFileSpeed / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB/s';
|
||||
}
|
||||
}
|
||||
|
||||
String getAssetCreationDate() {
|
||||
return DateFormat.yMMMMd().format(
|
||||
DateTime.parse(
|
||||
asset.fileCreatedAt.toString(),
|
||||
).toLocal(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildErrorChip() {
|
||||
return ActionChip(
|
||||
avatar: Icon(
|
||||
Icons.info,
|
||||
color: Colors.red[400],
|
||||
),
|
||||
elevation: 1,
|
||||
visualDensity: VisualDensity.compact,
|
||||
label: Text(
|
||||
"backup_controller_page_failed",
|
||||
style: TextStyle(
|
||||
color: Colors.red[400],
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
).tr(
|
||||
args: [ref.watch(errorBackupListProvider).length.toString()],
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
onPressed: () => context.pushRoute(const FailedBackupStatusRoute()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAssetInfoTable() {
|
||||
return Table(
|
||||
border: TableBorder.all(
|
||||
color: context.colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
leading: Icon(
|
||||
Icons.image_outlined,
|
||||
color: context.primaryColor,
|
||||
size: 30,
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text(
|
||||
'backup_controller_page_filename',
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
).tr(
|
||||
args: [asset.fileName, asset.fileType.toLowerCase()],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text(
|
||||
"backup_controller_page_created",
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
).tr(
|
||||
args: [getAssetCreationDate()],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text(
|
||||
"backup_controller_page_id",
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
).tr(args: [asset.id]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"backup_controller_page_uploading_file_info",
|
||||
style: context.textTheme.titleSmall,
|
||||
).tr(),
|
||||
const BackupErrorChip(),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
children: [
|
||||
if (Platform.isIOS) const IcloudDownloadProgressBar(),
|
||||
const BackupUploadProgressBar(),
|
||||
const BackupUploadStats(),
|
||||
const BackupAssetInfoTable(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
buildiCloudDownloadProgerssBar() {
|
||||
if (asset.iCloudAsset != null && asset.iCloudAsset!) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 110,
|
||||
child: Text(
|
||||
"iCloud Download",
|
||||
style: context.textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 10.0,
|
||||
value: uploadProgress / 100.0,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
" ${iCloudDownloadProgress.toStringAsFixed(0)}%",
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
buildUploadProgressBar() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (asset.iCloudAsset != null && asset.iCloudAsset!)
|
||||
SizedBox(
|
||||
width: 110,
|
||||
child: Text(
|
||||
"Immich Upload",
|
||||
style: context.textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 10.0,
|
||||
value: uploadProgress / 100.0,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
" ${uploadProgress.toStringAsFixed(0)}%",
|
||||
style: const TextStyle(fontSize: 12, fontFamily: "OverpassMono"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildUploadStats() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 2.0, bottom: 2.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
uploadFileProgress,
|
||||
style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"),
|
||||
),
|
||||
Text(
|
||||
formatUploadFileSpeed(uploadFileSpeed),
|
||||
style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return FutureBuilder<Asset?>(
|
||||
future: ref.read(assetMediaRepositoryProvider).get(asset.id),
|
||||
builder: (context, thumbnail) => ListTile(
|
||||
isThreeLine: true,
|
||||
leading: AnimatedCrossFade(
|
||||
alignment: Alignment.centerLeft,
|
||||
firstChild: GestureDetector(
|
||||
onTap: () => isShowThumbnail.value = false,
|
||||
child: thumbnail.hasData
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: ImmichThumbnail(
|
||||
asset: thumbnail.data,
|
||||
width: 50,
|
||||
height: 50,
|
||||
),
|
||||
)
|
||||
: const SizedBox(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
secondChild: GestureDetector(
|
||||
onTap: () => isShowThumbnail.value = true,
|
||||
child: Icon(
|
||||
Icons.image_outlined,
|
||||
color: context.primaryColor,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
crossFadeState: isShowThumbnail.value
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"backup_controller_page_uploading_file_info",
|
||||
style: context.textTheme.titleSmall,
|
||||
).tr(),
|
||||
if (ref.watch(errorBackupListProvider).isNotEmpty) buildErrorChip(),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
children: [
|
||||
if (Platform.isIOS) buildiCloudDownloadProgerssBar(),
|
||||
buildUploadProgressBar(),
|
||||
buildUploadStats(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: buildAssetInfoTable(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
32
mobile/lib/widgets/backup/error_chip.dart
Normal file
32
mobile/lib/widgets/backup/error_chip.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/backup/error_chip_text.dart';
|
||||
|
||||
class BackupErrorChip extends ConsumerWidget {
|
||||
const BackupErrorChip({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final hasErrors =
|
||||
ref.watch(errorBackupListProvider.select((value) => value.isNotEmpty));
|
||||
if (!hasErrors) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return ActionChip(
|
||||
avatar: const Icon(
|
||||
Icons.info,
|
||||
color: red400,
|
||||
),
|
||||
elevation: 1,
|
||||
visualDensity: VisualDensity.compact,
|
||||
label: const BackupErrorChipText(),
|
||||
backgroundColor: Colors.white,
|
||||
onPressed: () => context.pushRoute(const FailedBackupStatusRoute()),
|
||||
);
|
||||
}
|
||||
}
|
28
mobile/lib/widgets/backup/error_chip_text.dart
Normal file
28
mobile/lib/widgets/backup/error_chip_text.dart
Normal file
@ -0,0 +1,28 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||
|
||||
class BackupErrorChipText extends ConsumerWidget {
|
||||
const BackupErrorChipText({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final count = ref.watch(errorBackupListProvider).length;
|
||||
if (count == 0) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return const Text(
|
||||
"backup_controller_page_failed",
|
||||
style: TextStyle(
|
||||
color: red400,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
).tr(
|
||||
args: [count.toString()],
|
||||
);
|
||||
}
|
||||
}
|
61
mobile/lib/widgets/backup/icloud_download_progress_bar.dart
Normal file
61
mobile/lib/widgets/backup/icloud_download_progress_bar.dart
Normal file
@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
|
||||
class IcloudDownloadProgressBar extends ConsumerWidget {
|
||||
const IcloudDownloadProgressBar({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isManualUpload = ref.watch(
|
||||
backupProvider.select(
|
||||
(value) => value.backupProgress == BackUpProgressEnum.manualInProgress,
|
||||
),
|
||||
);
|
||||
|
||||
final isIcloudAsset = isManualUpload
|
||||
? ref.watch(
|
||||
manualUploadProvider
|
||||
.select((value) => value.currentUploadAsset.isIcloudAsset),
|
||||
)
|
||||
: ref.watch(
|
||||
backupProvider
|
||||
.select((value) => value.currentUploadAsset.isIcloudAsset),
|
||||
);
|
||||
|
||||
if (!isIcloudAsset) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final iCloudDownloadProgress = ref
|
||||
.watch(backupProvider.select((value) => value.iCloudDownloadProgress));
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 110,
|
||||
child: Text(
|
||||
"iCloud Download",
|
||||
style: context.textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 10.0,
|
||||
value: iCloudDownloadProgress / 100.0,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
" ${iCloudDownloadProgress ~/ 1}%",
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
64
mobile/lib/widgets/backup/upload_progress_bar.dart
Normal file
64
mobile/lib/widgets/backup/upload_progress_bar.dart
Normal file
@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
|
||||
class BackupUploadProgressBar extends ConsumerWidget {
|
||||
const BackupUploadProgressBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isManualUpload = ref.watch(
|
||||
backupProvider.select(
|
||||
(value) => value.backupProgress == BackUpProgressEnum.manualInProgress,
|
||||
),
|
||||
);
|
||||
|
||||
final isIcloudAsset = isManualUpload
|
||||
? ref.watch(
|
||||
manualUploadProvider
|
||||
.select((value) => value.currentUploadAsset.isIcloudAsset),
|
||||
)
|
||||
: ref.watch(
|
||||
backupProvider
|
||||
.select((value) => value.currentUploadAsset.isIcloudAsset),
|
||||
);
|
||||
|
||||
final uploadProgress = isManualUpload
|
||||
? ref.watch(
|
||||
manualUploadProvider.select((value) => value.progressInPercentage),
|
||||
)
|
||||
: ref.watch(
|
||||
backupProvider.select((value) => value.progressInPercentage),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (isIcloudAsset)
|
||||
SizedBox(
|
||||
width: 110,
|
||||
child: Text(
|
||||
"Immich Upload",
|
||||
style: context.textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 10.0,
|
||||
value: uploadProgress / 100.0,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
" ${uploadProgress.toStringAsFixed(0)}%",
|
||||
style: const TextStyle(fontSize: 12, fontFamily: "OverpassMono"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
62
mobile/lib/widgets/backup/upload_stats.dart
Normal file
62
mobile/lib/widgets/backup/upload_stats.dart
Normal file
@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
|
||||
class BackupUploadStats extends ConsumerWidget {
|
||||
const BackupUploadStats({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isManualUpload = ref.watch(
|
||||
backupProvider.select(
|
||||
(value) => value.backupProgress == BackUpProgressEnum.manualInProgress,
|
||||
),
|
||||
);
|
||||
|
||||
final uploadFileProgress = isManualUpload
|
||||
? ref.watch(
|
||||
manualUploadProvider.select((value) => value.progressInFileSize),
|
||||
)
|
||||
: ref.watch(backupProvider.select((value) => value.progressInFileSize));
|
||||
|
||||
final uploadFileSpeed = isManualUpload
|
||||
? ref.watch(
|
||||
manualUploadProvider.select((value) => value.progressInFileSpeed),
|
||||
)
|
||||
: ref.watch(
|
||||
backupProvider.select((value) => value.progressInFileSpeed),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 2.0, bottom: 2.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
uploadFileProgress,
|
||||
style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"),
|
||||
),
|
||||
Text(
|
||||
_formatUploadFileSpeed(uploadFileSpeed),
|
||||
style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
String _formatUploadFileSpeed(double uploadFileSpeed) {
|
||||
if (uploadFileSpeed < 1024) {
|
||||
return '${uploadFileSpeed.toStringAsFixed(2)} B/s';
|
||||
} else if (uploadFileSpeed < 1024 * 1024) {
|
||||
return '${(uploadFileSpeed / 1024).toStringAsFixed(2)} KB/s';
|
||||
} else if (uploadFileSpeed < 1024 * 1024 * 1024) {
|
||||
return '${(uploadFileSpeed / (1024 * 1024)).toStringAsFixed(2)} MB/s';
|
||||
} else {
|
||||
return '${(uploadFileSpeed / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB/s';
|
||||
}
|
||||
}
|
||||
}
|
BIN
mobile/openapi/lib/model/server_stats_response_dto.dart
generated
BIN
mobile/openapi/lib/model/server_stats_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/usage_by_user_dto.dart
generated
BIN
mobile/openapi/lib/model/usage_by_user_dto.dart
generated
Binary file not shown.
@ -11017,7 +11017,9 @@
|
||||
{
|
||||
"photos": 1,
|
||||
"videos": 1,
|
||||
"diskUsageRaw": 1
|
||||
"diskUsageRaw": 2,
|
||||
"usagePhotos": 1,
|
||||
"usageVideos": 1
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
@ -11026,6 +11028,16 @@
|
||||
"title": "Array of usage for each user",
|
||||
"type": "array"
|
||||
},
|
||||
"usagePhotos": {
|
||||
"default": 0,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"usageVideos": {
|
||||
"default": 0,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"videos": {
|
||||
"default": 0,
|
||||
"type": "integer"
|
||||
@ -11035,6 +11047,8 @@
|
||||
"photos",
|
||||
"usage",
|
||||
"usageByUser",
|
||||
"usagePhotos",
|
||||
"usageVideos",
|
||||
"videos"
|
||||
],
|
||||
"type": "object"
|
||||
@ -12614,6 +12628,14 @@
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"usagePhotos": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"usageVideos": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -12628,6 +12650,8 @@
|
||||
"photos",
|
||||
"quotaSizeInBytes",
|
||||
"usage",
|
||||
"usagePhotos",
|
||||
"usageVideos",
|
||||
"userId",
|
||||
"userName",
|
||||
"videos"
|
||||
|
@ -976,6 +976,8 @@ export type UsageByUserDto = {
|
||||
photos: number;
|
||||
quotaSizeInBytes: number | null;
|
||||
usage: number;
|
||||
usagePhotos: number;
|
||||
usageVideos: number;
|
||||
userId: string;
|
||||
userName: string;
|
||||
videos: number;
|
||||
@ -984,6 +986,8 @@ export type ServerStatsResponseDto = {
|
||||
photos: number;
|
||||
usage: number;
|
||||
usageByUser: UsageByUserDto[];
|
||||
usagePhotos: number;
|
||||
usageVideos: number;
|
||||
videos: number;
|
||||
};
|
||||
export type ServerStorageResponseDto = {
|
||||
|
@ -86,6 +86,10 @@ export class UsageByUserDto {
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usage!: number;
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usagePhotos!: number;
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usageVideos!: number;
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
quotaSizeInBytes!: number | null;
|
||||
}
|
||||
|
||||
@ -99,6 +103,12 @@ export class ServerStatsResponseDto {
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usage = 0;
|
||||
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usagePhotos = 0;
|
||||
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usageVideos = 0;
|
||||
|
||||
@ApiProperty({
|
||||
isArray: true,
|
||||
type: UsageByUserDto,
|
||||
@ -107,7 +117,9 @@ export class ServerStatsResponseDto {
|
||||
{
|
||||
photos: 1,
|
||||
videos: 1,
|
||||
diskUsageRaw: 1,
|
||||
diskUsageRaw: 2,
|
||||
usagePhotos: 1,
|
||||
usageVideos: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
@ -11,6 +11,8 @@ export interface UserStatsQueryResponse {
|
||||
photos: number;
|
||||
videos: number;
|
||||
usage: number;
|
||||
usagePhotos: number;
|
||||
usageVideos: number;
|
||||
quotaSizeInBytes: number | null;
|
||||
}
|
||||
|
||||
|
@ -140,7 +140,23 @@ SELECT
|
||||
"assets"."libraryId" IS NULL
|
||||
),
|
||||
0
|
||||
) AS "usage"
|
||||
) AS "usage",
|
||||
COALESCE(
|
||||
SUM("exif"."fileSizeInByte") FILTER (
|
||||
WHERE
|
||||
"assets"."libraryId" IS NULL
|
||||
AND "assets"."type" = 'IMAGE'
|
||||
),
|
||||
0
|
||||
) AS "usagePhotos",
|
||||
COALESCE(
|
||||
SUM("exif"."fileSizeInByte") FILTER (
|
||||
WHERE
|
||||
"assets"."libraryId" IS NULL
|
||||
AND "assets"."type" = 'VIDEO'
|
||||
),
|
||||
0
|
||||
) AS "usageVideos"
|
||||
FROM
|
||||
"users" "users"
|
||||
LEFT JOIN "assets" "assets" ON "assets"."ownerId" = "users"."id"
|
||||
|
@ -108,6 +108,14 @@ export class UserRepository implements IUserRepository {
|
||||
.addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos')
|
||||
.addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos')
|
||||
.addSelect('COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL), 0)', 'usage')
|
||||
.addSelect(
|
||||
`COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL AND assets.type = 'IMAGE'), 0)`,
|
||||
'usagePhotos',
|
||||
)
|
||||
.addSelect(
|
||||
`COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL AND assets.type = 'VIDEO'), 0)`,
|
||||
'usageVideos',
|
||||
)
|
||||
.addSelect('users.quotaSizeInBytes', 'quotaSizeInBytes')
|
||||
.leftJoin('users.assets', 'assets')
|
||||
.leftJoin('assets.exifInfo', 'exif')
|
||||
@ -119,6 +127,8 @@ export class UserRepository implements IUserRepository {
|
||||
stat.photos = Number(stat.photos);
|
||||
stat.videos = Number(stat.videos);
|
||||
stat.usage = Number(stat.usage);
|
||||
stat.usagePhotos = Number(stat.usagePhotos);
|
||||
stat.usageVideos = Number(stat.usageVideos);
|
||||
stat.quotaSizeInBytes = stat.quotaSizeInBytes;
|
||||
}
|
||||
|
||||
|
@ -185,6 +185,8 @@ describe(ServerService.name, () => {
|
||||
photos: 10,
|
||||
videos: 11,
|
||||
usage: 12_345,
|
||||
usagePhotos: 1,
|
||||
usageVideos: 11_345,
|
||||
quotaSizeInBytes: 0,
|
||||
},
|
||||
{
|
||||
@ -193,6 +195,8 @@ describe(ServerService.name, () => {
|
||||
photos: 10,
|
||||
videos: 20,
|
||||
usage: 123_456,
|
||||
usagePhotos: 100,
|
||||
usageVideos: 23_456,
|
||||
quotaSizeInBytes: 0,
|
||||
},
|
||||
{
|
||||
@ -201,6 +205,8 @@ describe(ServerService.name, () => {
|
||||
photos: 100,
|
||||
videos: 0,
|
||||
usage: 987_654,
|
||||
usagePhotos: 900,
|
||||
usageVideos: 87_654,
|
||||
quotaSizeInBytes: 0,
|
||||
},
|
||||
]);
|
||||
@ -209,11 +215,15 @@ describe(ServerService.name, () => {
|
||||
photos: 120,
|
||||
videos: 31,
|
||||
usage: 1_123_455,
|
||||
usagePhotos: 1001,
|
||||
usageVideos: 122_455,
|
||||
usageByUser: [
|
||||
{
|
||||
photos: 10,
|
||||
quotaSizeInBytes: 0,
|
||||
usage: 12_345,
|
||||
usagePhotos: 1,
|
||||
usageVideos: 11_345,
|
||||
userName: '1 User',
|
||||
userId: 'user1',
|
||||
videos: 11,
|
||||
@ -222,6 +232,8 @@ describe(ServerService.name, () => {
|
||||
photos: 10,
|
||||
quotaSizeInBytes: 0,
|
||||
usage: 123_456,
|
||||
usagePhotos: 100,
|
||||
usageVideos: 23_456,
|
||||
userName: '2 User',
|
||||
userId: 'user2',
|
||||
videos: 20,
|
||||
@ -230,6 +242,8 @@ describe(ServerService.name, () => {
|
||||
photos: 100,
|
||||
quotaSizeInBytes: 0,
|
||||
usage: 987_654,
|
||||
usagePhotos: 900,
|
||||
usageVideos: 87_654,
|
||||
userName: '3 User',
|
||||
userId: 'user3',
|
||||
videos: 0,
|
||||
|
@ -126,11 +126,16 @@ export class ServerService extends BaseService {
|
||||
usage.photos = user.photos;
|
||||
usage.videos = user.videos;
|
||||
usage.usage = user.usage;
|
||||
usage.usagePhotos = user.usagePhotos;
|
||||
usage.usageVideos = user.usageVideos;
|
||||
usage.quotaSizeInBytes = user.quotaSizeInBytes;
|
||||
|
||||
serverStats.photos += usage.photos;
|
||||
serverStats.videos += usage.videos;
|
||||
serverStats.usage += usage.usage;
|
||||
serverStats.usagePhotos += usage.usagePhotos;
|
||||
serverStats.usageVideos += usage.usageVideos;
|
||||
|
||||
serverStats.usageByUser.push(usage);
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,8 @@
|
||||
photos: 0,
|
||||
videos: 0,
|
||||
usage: 0,
|
||||
usagePhotos: 0,
|
||||
usageVideos: 0,
|
||||
usageByUser: [],
|
||||
},
|
||||
}: Props = $props();
|
||||
@ -105,8 +107,12 @@
|
||||
class="flex h-[50px] w-full place-items-center text-center odd:bg-immich-gray even:bg-immich-bg odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50"
|
||||
>
|
||||
<td class="w-1/4 text-ellipsis px-2 text-sm">{user.userName}</td>
|
||||
<td class="w-1/4 text-ellipsis px-2 text-sm">{user.photos.toLocaleString($locale)}</td>
|
||||
<td class="w-1/4 text-ellipsis px-2 text-sm">{user.videos.toLocaleString($locale)}</td>
|
||||
<td class="w-1/4 text-ellipsis px-2 text-sm"
|
||||
>{user.photos.toLocaleString($locale)} ({getByteUnitString(user.usagePhotos, $locale, 0)})</td
|
||||
>
|
||||
<td class="w-1/4 text-ellipsis px-2 text-sm"
|
||||
>{user.videos.toLocaleString($locale)} ({getByteUnitString(user.usageVideos, $locale, 0)})</td
|
||||
>
|
||||
<td class="w-1/4 text-ellipsis px-2 text-sm">
|
||||
{getByteUnitString(user.usage, $locale, 0)}
|
||||
{#if user.quotaSizeInBytes}
|
||||
|
@ -58,7 +58,6 @@
|
||||
const handleToggle = (user: UserResponseDto) => {
|
||||
if (Object.keys(selectedUsers).includes(user.id)) {
|
||||
delete selectedUsers[user.id];
|
||||
selectedUsers = selectedUsers;
|
||||
} else {
|
||||
selectedUsers[user.id] = { user, role: AlbumUserRole.Editor };
|
||||
}
|
||||
@ -67,7 +66,6 @@
|
||||
const handleChangeRole = (user: UserResponseDto, role: AlbumUserRole | 'none') => {
|
||||
if (role === 'none') {
|
||||
delete selectedUsers[user.id];
|
||||
selectedUsers = selectedUsers;
|
||||
} else {
|
||||
selectedUsers[user.id].role = role;
|
||||
}
|
||||
|
@ -110,7 +110,6 @@
|
||||
try {
|
||||
await deleteActivity({ id: reaction.id });
|
||||
reactions.splice(index, 1);
|
||||
reactions = reactions;
|
||||
if (isLiked && reaction.type === ReactionType.Like && reaction.id == isLiked.id) {
|
||||
onDeleteLike();
|
||||
} else {
|
||||
@ -143,8 +142,6 @@
|
||||
|
||||
message = '';
|
||||
onAddComment();
|
||||
// Re-render the activity feed
|
||||
reactions = reactions;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_add_comment'));
|
||||
} finally {
|
||||
|
@ -43,10 +43,10 @@
|
||||
import DetailPanel from './detail-panel.svelte';
|
||||
import CropArea from './editor/crop-tool/crop-area.svelte';
|
||||
import EditorPanel from './editor/editor-panel.svelte';
|
||||
import PanoramaViewer from './panorama-viewer.svelte';
|
||||
import PhotoViewer from './photo-viewer.svelte';
|
||||
import SlideshowBar from './slideshow-bar.svelte';
|
||||
import VideoViewer from './video-wrapper-viewer.svelte';
|
||||
import ImagePanoramaViewer from './image-panorama-viewer.svelte';
|
||||
|
||||
interface Props {
|
||||
assetStore?: AssetStore | null;
|
||||
@ -512,7 +512,7 @@
|
||||
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath
|
||||
.toLowerCase()
|
||||
.endsWith('.insp'))}
|
||||
<PanoramaViewer {asset} />
|
||||
<ImagePanoramaViewer {asset} />
|
||||
{:else if isShowEditor && selectedEditType === 'crop'}
|
||||
<CropArea {asset} />
|
||||
{:else}
|
||||
|
@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { getAssetOriginalUrl, getKey } from '$lib/utils';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { AssetMediaSize, viewAsset, type AssetResponseDto } from '@immich/sdk';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
}
|
||||
|
||||
const { asset }: Props = $props();
|
||||
|
||||
const loadAssetData = async (id: string) => {
|
||||
const data = await viewAsset({ id, size: AssetMediaSize.Preview, key: getKey() });
|
||||
return URL.createObjectURL(data);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
|
||||
{#await Promise.all([loadAssetData(asset.id), import('./photo-sphere-viewer-adapter.svelte')])}
|
||||
<LoadingSpinner />
|
||||
{:then [data, { default: PhotoSphereViewer }]}
|
||||
<PhotoSphereViewer
|
||||
panorama={data}
|
||||
originalImageUrl={isWebCompatibleImage(asset) ? getAssetOriginalUrl(asset.id) : undefined}
|
||||
/>
|
||||
{:catch}
|
||||
{$t('errors.failed_to_load_asset')}
|
||||
{/await}
|
||||
</div>
|
@ -1,54 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
||||
import { getAssetOriginalUrl, getKey } from '$lib/utils';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { AssetMediaSize, AssetTypeEnum, viewAsset, type AssetResponseDto } from '@immich/sdk';
|
||||
import type { AdapterConstructor, PluginConstructor } from '@photo-sphere-viewer/core';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
|
||||
interface Props {
|
||||
asset: { id: string; type: AssetTypeEnum.Video } | AssetResponseDto;
|
||||
}
|
||||
|
||||
let { asset }: Props = $props();
|
||||
|
||||
const photoSphereConfigs =
|
||||
asset.type === AssetTypeEnum.Video
|
||||
? ([
|
||||
import('@photo-sphere-viewer/equirectangular-video-adapter').then(
|
||||
({ EquirectangularVideoAdapter }) => EquirectangularVideoAdapter,
|
||||
),
|
||||
import('@photo-sphere-viewer/video-plugin').then(({ VideoPlugin }) => [VideoPlugin]),
|
||||
true,
|
||||
import('@photo-sphere-viewer/video-plugin/index.css'),
|
||||
] as [PromiseLike<AdapterConstructor>, Promise<PluginConstructor[]>, true, unknown])
|
||||
: ([undefined, [], false] as [undefined, [], false]);
|
||||
|
||||
const originalImageUrl =
|
||||
asset.type === AssetTypeEnum.Image && isWebCompatibleImage(asset) ? getAssetOriginalUrl(asset.id) : null;
|
||||
|
||||
const loadAssetData = async () => {
|
||||
if (asset.type === AssetTypeEnum.Video) {
|
||||
return { source: getAssetOriginalUrl(asset.id) };
|
||||
}
|
||||
if (originalImageUrl && $alwaysLoadOriginalFile) {
|
||||
return getAssetOriginalUrl(asset.id);
|
||||
}
|
||||
const data = await viewAsset({ id: asset.id, size: AssetMediaSize.Preview, key: getKey() });
|
||||
const url = URL.createObjectURL(data);
|
||||
return url;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
|
||||
<!-- the photo sphere viewer is quite large, so lazy load it in parallel with loading the data -->
|
||||
{#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte'), ...photoSphereConfigs])}
|
||||
<LoadingSpinner />
|
||||
{:then [data, module, adapter, plugins, navbar]}
|
||||
<module.default panorama={data} plugins={plugins ?? undefined} {navbar} {adapter} {originalImageUrl} />
|
||||
{:catch}
|
||||
{$t('errors.failed_to_load_asset')}
|
||||
{/await}
|
||||
</div>
|
@ -12,7 +12,7 @@
|
||||
|
||||
interface Props {
|
||||
panorama: string | { source: string };
|
||||
originalImageUrl: string | null;
|
||||
originalImageUrl?: string;
|
||||
adapter?: AdapterConstructor | [AdapterConstructor, unknown];
|
||||
plugins?: (PluginConstructor | [PluginConstructor, unknown])[];
|
||||
navbar?: boolean;
|
||||
|
@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { getAssetOriginalUrl } from '$lib/utils';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
assetId: string;
|
||||
}
|
||||
|
||||
const { assetId }: Props = $props();
|
||||
|
||||
const modules = Promise.all([
|
||||
import('./photo-sphere-viewer-adapter.svelte').then((module) => module.default),
|
||||
import('@photo-sphere-viewer/equirectangular-video-adapter').then((module) => module.EquirectangularVideoAdapter),
|
||||
import('@photo-sphere-viewer/video-plugin').then((module) => module.VideoPlugin),
|
||||
import('@photo-sphere-viewer/video-plugin/index.css'),
|
||||
]);
|
||||
</script>
|
||||
|
||||
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
|
||||
{#await modules}
|
||||
<LoadingSpinner />
|
||||
{:then [PhotoSphereViewer, adapter, videoPlugin]}
|
||||
<PhotoSphereViewer panorama={{ source: getAssetOriginalUrl(assetId) }} plugins={[videoPlugin]} {adapter} navbar />
|
||||
{:catch}
|
||||
{$t('errors.failed_to_load_asset')}
|
||||
{/await}
|
||||
</div>
|
@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { AssetTypeEnum } from '@immich/sdk';
|
||||
import { ProjectionType } from '$lib/constants';
|
||||
import VideoNativeViewer from '$lib/components/asset-viewer/video-native-viewer.svelte';
|
||||
import PanoramaViewer from '$lib/components/asset-viewer/panorama-viewer.svelte';
|
||||
import VideoPanoramaViewer from '$lib/components/asset-viewer/video-panorama-viewer.svelte';
|
||||
|
||||
interface Props {
|
||||
assetId: string;
|
||||
@ -30,7 +29,7 @@
|
||||
</script>
|
||||
|
||||
{#if projectionType === ProjectionType.EQUIRECTANGULAR}
|
||||
<PanoramaViewer asset={{ id: assetId, type: AssetTypeEnum.Video }} />
|
||||
<VideoPanoramaViewer {assetId} />
|
||||
{:else}
|
||||
<VideoNativeViewer
|
||||
{loopVideo}
|
||||
|
@ -90,7 +90,6 @@
|
||||
for (const person of people) {
|
||||
person.isHidden = personIsHidden[person.id];
|
||||
}
|
||||
people = people;
|
||||
|
||||
onClose();
|
||||
} catch (error) {
|
||||
|
@ -101,15 +101,9 @@
|
||||
const handleReset = (id: string) => {
|
||||
if (selectedPersonToReassign[id]) {
|
||||
delete selectedPersonToReassign[id];
|
||||
|
||||
// trigger reactivity
|
||||
selectedPersonToReassign = selectedPersonToReassign;
|
||||
}
|
||||
if (selectedPersonToCreate[id]) {
|
||||
delete selectedPersonToCreate[id];
|
||||
|
||||
// trigger reactivity
|
||||
selectedPersonToCreate = selectedPersonToCreate;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -35,12 +35,10 @@
|
||||
}
|
||||
|
||||
selectedIds.add(option.value);
|
||||
selectedIds = selectedIds;
|
||||
};
|
||||
|
||||
const handleRemove = (tag: string) => {
|
||||
selectedIds.delete(tag);
|
||||
selectedIds = selectedIds;
|
||||
};
|
||||
|
||||
const onsubmit = (event: Event) => {
|
||||
|
@ -112,7 +112,6 @@
|
||||
assets.findIndex((a) => a.id === action.asset.id),
|
||||
1,
|
||||
);
|
||||
assets = assets;
|
||||
if (assets.length === 0) {
|
||||
await goto(AppRoute.PHOTOS);
|
||||
} else if (currentViewAssetIndex === assets.length) {
|
||||
|
@ -7,7 +7,7 @@
|
||||
export type SearchFilter = {
|
||||
query: string;
|
||||
queryType: 'smart' | 'metadata';
|
||||
personIds: Set<string>;
|
||||
personIds: SvelteSet<string>;
|
||||
location: SearchLocationFilter;
|
||||
camera: SearchCameraFilter;
|
||||
date: SearchDateFilter;
|
||||
|
@ -9,9 +9,10 @@
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { t } from 'svelte-i18n';
|
||||
import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte';
|
||||
import type { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
interface Props {
|
||||
selectedPeople: Set<string>;
|
||||
selectedPeople: SvelteSet<string>;
|
||||
}
|
||||
|
||||
let { selectedPeople = $bindable() }: Props = $props();
|
||||
@ -43,7 +44,6 @@
|
||||
} else {
|
||||
selectedPeople.add(id);
|
||||
}
|
||||
selectedPeople = selectedPeople;
|
||||
}
|
||||
|
||||
const filterPeople = (list: PersonResponseDto[], name: string) => {
|
||||
|
@ -117,7 +117,6 @@
|
||||
await updatePartner({ id: partner.user.id, updatePartnerDto: { inTimeline } });
|
||||
|
||||
partner.inTimeline = inTimeline;
|
||||
partners = partners;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_update_timeline_display_status'));
|
||||
}
|
||||
|
@ -35,7 +35,6 @@
|
||||
}
|
||||
|
||||
selectedAssetIds.add(suggestedAsset.id);
|
||||
selectedAssetIds = selectedAssetIds;
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@ -48,13 +47,10 @@
|
||||
} else {
|
||||
selectedAssetIds.add(asset.id);
|
||||
}
|
||||
|
||||
selectedAssetIds = selectedAssetIds;
|
||||
};
|
||||
|
||||
const onSelectNone = () => {
|
||||
selectedAssetIds.clear();
|
||||
selectedAssetIds = selectedAssetIds;
|
||||
};
|
||||
|
||||
const onSelectAll = () => {
|
||||
|
@ -35,8 +35,6 @@
|
||||
person.updatedAt = Date.now().toString();
|
||||
}
|
||||
});
|
||||
// trigger reactivity
|
||||
people = people;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -74,9 +74,6 @@
|
||||
person.updatedAt = new Date().toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
// trigger reactivity
|
||||
people = people;
|
||||
});
|
||||
});
|
||||
|
||||
@ -146,9 +143,6 @@
|
||||
message: $t('change_name_successfully'),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
|
||||
// trigger reactivity
|
||||
people = people;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_save_name'));
|
||||
}
|
||||
|
@ -156,8 +156,6 @@
|
||||
|
||||
searchResultAlbums.push(...albums.items);
|
||||
searchResultAssets.push(...assets.items);
|
||||
searchResultAlbums = searchResultAlbums;
|
||||
searchResultAssets = searchResultAssets;
|
||||
|
||||
nextPage = assets.nextPage ? Number(assets.nextPage) : null;
|
||||
} catch (error) {
|
||||
|
Loading…
Reference in New Issue
Block a user