You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-07-04 05:50:38 +02:00
Merge branch 'main' into feat/notification-email-template
This commit is contained in:
@ -98,6 +98,10 @@ SELECT * FROM "move_history";
|
|||||||
SELECT * FROM "users";
|
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
|
## System Config
|
||||||
|
|
||||||
```sql title="Custom settings"
|
```sql title="Custom settings"
|
||||||
|
@ -163,11 +163,15 @@ describe('/server', () => {
|
|||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
photos: 0,
|
photos: 0,
|
||||||
usage: 0,
|
usage: 0,
|
||||||
|
usagePhotos: 0,
|
||||||
|
usageVideos: 0,
|
||||||
usageByUser: [
|
usageByUser: [
|
||||||
{
|
{
|
||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
photos: 0,
|
photos: 0,
|
||||||
usage: 0,
|
usage: 0,
|
||||||
|
usagePhotos: 0,
|
||||||
|
usageVideos: 0,
|
||||||
userName: 'Immich Admin',
|
userName: 'Immich Admin',
|
||||||
userId: admin.userId,
|
userId: admin.userId,
|
||||||
videos: 0,
|
videos: 0,
|
||||||
@ -176,6 +180,8 @@ describe('/server', () => {
|
|||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
photos: 0,
|
photos: 0,
|
||||||
usage: 0,
|
usage: 0,
|
||||||
|
usagePhotos: 0,
|
||||||
|
usageVideos: 0,
|
||||||
userName: 'User 1',
|
userName: 'User 1',
|
||||||
userId: nonAdmin.userId,
|
userId: nonAdmin.userId,
|
||||||
videos: 0,
|
videos: 0,
|
||||||
|
@ -21,6 +21,7 @@ const Color immichBrandColorLight = Color(0xFF4150AF);
|
|||||||
const Color immichBrandColorDark = Color(0xFFACCBFA);
|
const Color immichBrandColorDark = Color(0xFFACCBFA);
|
||||||
const Color whiteOpacity75 = Color.fromARGB((0.75 * 255) ~/ 1, 255, 255, 255);
|
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 blackOpacity90 = Color.fromARGB((0.90 * 255) ~/ 1, 0, 0, 0);
|
||||||
|
const Color red400 = Color(0xFFEF5350);
|
||||||
|
|
||||||
final Map<ImmichColorPreset, ImmichTheme> _themePresetsMap = {
|
final Map<ImmichColorPreset, ImmichTheme> _themePresetsMap = {
|
||||||
ImmichColorPreset.indigo: ImmichTheme(
|
ImmichColorPreset.indigo: ImmichTheme(
|
||||||
|
@ -18,6 +18,9 @@ class CurrentUploadAsset {
|
|||||||
this.iCloudAsset,
|
this.iCloudAsset,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
bool get isIcloudAsset => iCloudAsset != null && iCloudAsset!;
|
||||||
|
|
||||||
CurrentUploadAsset copyWith({
|
CurrentUploadAsset copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
DateTime? fileCreatedAt,
|
DateTime? fileCreatedAt,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
|
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
|
||||||
import 'package:immich_mobile/services/partner.service.dart';
|
import 'package:immich_mobile/services/partner.service.dart';
|
||||||
@ -9,9 +10,19 @@ import 'package:isar/isar.dart';
|
|||||||
|
|
||||||
class PartnerSharedWithNotifier extends StateNotifier<List<User>> {
|
class PartnerSharedWithNotifier extends StateNotifier<List<User>> {
|
||||||
PartnerSharedWithNotifier(Isar db, this._ps) : super([]) {
|
PartnerSharedWithNotifier(Isar db, this._ps) : super([]) {
|
||||||
final query = db.users.filter().isPartnerSharedWithEqualTo(true);
|
Function eq = const ListEquality<User>().equals;
|
||||||
query.findAll().then((partners) => state = partners);
|
final query = db.users.filter().isPartnerSharedWithEqualTo(true).sortById();
|
||||||
query.watch().listen((partners) => state = partners);
|
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}) {
|
Future<bool> updatePartner(User partner, {required bool inTimeline}) {
|
||||||
@ -31,9 +42,19 @@ final partnerSharedWithProvider =
|
|||||||
|
|
||||||
class PartnerSharedByNotifier extends StateNotifier<List<User>> {
|
class PartnerSharedByNotifier extends StateNotifier<List<User>> {
|
||||||
PartnerSharedByNotifier(Isar db) : super([]) {
|
PartnerSharedByNotifier(Isar db) : super([]) {
|
||||||
final query = db.users.filter().isPartnerSharedByEqualTo(true);
|
Function eq = const ListEquality<User>().equals;
|
||||||
query.findAll().then((partners) => state = partners);
|
final query = db.users.filter().isPartnerSharedByEqualTo(true).sortById();
|
||||||
streamSub = query.watch().listen((partners) => state = partners);
|
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;
|
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,275 +1,26 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
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: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/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/widgets/backup/asset_info_table.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/widgets/backup/error_chip.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/widgets/backup/icloud_download_progress_bar.dart';
|
||||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
import 'package:immich_mobile/widgets/backup/upload_progress_bar.dart';
|
||||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
import 'package:immich_mobile/widgets/backup/upload_stats.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';
|
|
||||||
|
|
||||||
class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
class CurrentUploadingAssetInfoBox extends StatelessWidget {
|
||||||
const CurrentUploadingAssetInfoBox({super.key});
|
const CurrentUploadingAssetInfoBox({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
var isManualUpload = ref.watch(backupProvider).backupProgress ==
|
return ListTile(
|
||||||
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,
|
|
||||||
),
|
|
||||||
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]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
isThreeLine: true,
|
||||||
leading: AnimatedCrossFade(
|
leading: Icon(
|
||||||
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,
|
Icons.image_outlined,
|
||||||
color: context.primaryColor,
|
color: context.primaryColor,
|
||||||
size: 30,
|
size: 30,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
crossFadeState: isShowThumbnail.value
|
|
||||||
? CrossFadeState.showFirst
|
|
||||||
: CrossFadeState.showSecond,
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
),
|
|
||||||
title: Row(
|
title: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@ -277,21 +28,17 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
|||||||
"backup_controller_page_uploading_file_info",
|
"backup_controller_page_uploading_file_info",
|
||||||
style: context.textTheme.titleSmall,
|
style: context.textTheme.titleSmall,
|
||||||
).tr(),
|
).tr(),
|
||||||
if (ref.watch(errorBackupListProvider).isNotEmpty) buildErrorChip(),
|
const BackupErrorChip(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
children: [
|
children: [
|
||||||
if (Platform.isIOS) buildiCloudDownloadProgerssBar(),
|
if (Platform.isIOS) const IcloudDownloadProgressBar(),
|
||||||
buildUploadProgressBar(),
|
const BackupUploadProgressBar(),
|
||||||
buildUploadStats(),
|
const BackupUploadStats(),
|
||||||
Padding(
|
const BackupAssetInfoTable(),
|
||||||
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,8 @@ class ServerStatsResponseDto {
|
|||||||
this.photos = 0,
|
this.photos = 0,
|
||||||
this.usage = 0,
|
this.usage = 0,
|
||||||
this.usageByUser = const [],
|
this.usageByUser = const [],
|
||||||
|
this.usagePhotos = 0,
|
||||||
|
this.usageVideos = 0,
|
||||||
this.videos = 0,
|
this.videos = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -25,6 +27,10 @@ class ServerStatsResponseDto {
|
|||||||
|
|
||||||
List<UsageByUserDto> usageByUser;
|
List<UsageByUserDto> usageByUser;
|
||||||
|
|
||||||
|
int usagePhotos;
|
||||||
|
|
||||||
|
int usageVideos;
|
||||||
|
|
||||||
int videos;
|
int videos;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -32,6 +38,8 @@ class ServerStatsResponseDto {
|
|||||||
other.photos == photos &&
|
other.photos == photos &&
|
||||||
other.usage == usage &&
|
other.usage == usage &&
|
||||||
_deepEquality.equals(other.usageByUser, usageByUser) &&
|
_deepEquality.equals(other.usageByUser, usageByUser) &&
|
||||||
|
other.usagePhotos == usagePhotos &&
|
||||||
|
other.usageVideos == usageVideos &&
|
||||||
other.videos == videos;
|
other.videos == videos;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -40,16 +48,20 @@ class ServerStatsResponseDto {
|
|||||||
(photos.hashCode) +
|
(photos.hashCode) +
|
||||||
(usage.hashCode) +
|
(usage.hashCode) +
|
||||||
(usageByUser.hashCode) +
|
(usageByUser.hashCode) +
|
||||||
|
(usagePhotos.hashCode) +
|
||||||
|
(usageVideos.hashCode) +
|
||||||
(videos.hashCode);
|
(videos.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ServerStatsResponseDto[photos=$photos, usage=$usage, usageByUser=$usageByUser, videos=$videos]';
|
String toString() => 'ServerStatsResponseDto[photos=$photos, usage=$usage, usageByUser=$usageByUser, usagePhotos=$usagePhotos, usageVideos=$usageVideos, videos=$videos]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'photos'] = this.photos;
|
json[r'photos'] = this.photos;
|
||||||
json[r'usage'] = this.usage;
|
json[r'usage'] = this.usage;
|
||||||
json[r'usageByUser'] = this.usageByUser;
|
json[r'usageByUser'] = this.usageByUser;
|
||||||
|
json[r'usagePhotos'] = this.usagePhotos;
|
||||||
|
json[r'usageVideos'] = this.usageVideos;
|
||||||
json[r'videos'] = this.videos;
|
json[r'videos'] = this.videos;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@ -66,6 +78,8 @@ class ServerStatsResponseDto {
|
|||||||
photos: mapValueOfType<int>(json, r'photos')!,
|
photos: mapValueOfType<int>(json, r'photos')!,
|
||||||
usage: mapValueOfType<int>(json, r'usage')!,
|
usage: mapValueOfType<int>(json, r'usage')!,
|
||||||
usageByUser: UsageByUserDto.listFromJson(json[r'usageByUser']),
|
usageByUser: UsageByUserDto.listFromJson(json[r'usageByUser']),
|
||||||
|
usagePhotos: mapValueOfType<int>(json, r'usagePhotos')!,
|
||||||
|
usageVideos: mapValueOfType<int>(json, r'usageVideos')!,
|
||||||
videos: mapValueOfType<int>(json, r'videos')!,
|
videos: mapValueOfType<int>(json, r'videos')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -117,6 +131,8 @@ class ServerStatsResponseDto {
|
|||||||
'photos',
|
'photos',
|
||||||
'usage',
|
'usage',
|
||||||
'usageByUser',
|
'usageByUser',
|
||||||
|
'usagePhotos',
|
||||||
|
'usageVideos',
|
||||||
'videos',
|
'videos',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
18
mobile/openapi/lib/model/usage_by_user_dto.dart
generated
18
mobile/openapi/lib/model/usage_by_user_dto.dart
generated
@ -16,6 +16,8 @@ class UsageByUserDto {
|
|||||||
required this.photos,
|
required this.photos,
|
||||||
required this.quotaSizeInBytes,
|
required this.quotaSizeInBytes,
|
||||||
required this.usage,
|
required this.usage,
|
||||||
|
required this.usagePhotos,
|
||||||
|
required this.usageVideos,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.userName,
|
required this.userName,
|
||||||
required this.videos,
|
required this.videos,
|
||||||
@ -27,6 +29,10 @@ class UsageByUserDto {
|
|||||||
|
|
||||||
int usage;
|
int usage;
|
||||||
|
|
||||||
|
int usagePhotos;
|
||||||
|
|
||||||
|
int usageVideos;
|
||||||
|
|
||||||
String userId;
|
String userId;
|
||||||
|
|
||||||
String userName;
|
String userName;
|
||||||
@ -38,6 +44,8 @@ class UsageByUserDto {
|
|||||||
other.photos == photos &&
|
other.photos == photos &&
|
||||||
other.quotaSizeInBytes == quotaSizeInBytes &&
|
other.quotaSizeInBytes == quotaSizeInBytes &&
|
||||||
other.usage == usage &&
|
other.usage == usage &&
|
||||||
|
other.usagePhotos == usagePhotos &&
|
||||||
|
other.usageVideos == usageVideos &&
|
||||||
other.userId == userId &&
|
other.userId == userId &&
|
||||||
other.userName == userName &&
|
other.userName == userName &&
|
||||||
other.videos == videos;
|
other.videos == videos;
|
||||||
@ -48,12 +56,14 @@ class UsageByUserDto {
|
|||||||
(photos.hashCode) +
|
(photos.hashCode) +
|
||||||
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
|
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
|
||||||
(usage.hashCode) +
|
(usage.hashCode) +
|
||||||
|
(usagePhotos.hashCode) +
|
||||||
|
(usageVideos.hashCode) +
|
||||||
(userId.hashCode) +
|
(userId.hashCode) +
|
||||||
(userName.hashCode) +
|
(userName.hashCode) +
|
||||||
(videos.hashCode);
|
(videos.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UsageByUserDto[photos=$photos, quotaSizeInBytes=$quotaSizeInBytes, usage=$usage, userId=$userId, userName=$userName, videos=$videos]';
|
String toString() => 'UsageByUserDto[photos=$photos, quotaSizeInBytes=$quotaSizeInBytes, usage=$usage, usagePhotos=$usagePhotos, usageVideos=$usageVideos, userId=$userId, userName=$userName, videos=$videos]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -64,6 +74,8 @@ class UsageByUserDto {
|
|||||||
// json[r'quotaSizeInBytes'] = null;
|
// json[r'quotaSizeInBytes'] = null;
|
||||||
}
|
}
|
||||||
json[r'usage'] = this.usage;
|
json[r'usage'] = this.usage;
|
||||||
|
json[r'usagePhotos'] = this.usagePhotos;
|
||||||
|
json[r'usageVideos'] = this.usageVideos;
|
||||||
json[r'userId'] = this.userId;
|
json[r'userId'] = this.userId;
|
||||||
json[r'userName'] = this.userName;
|
json[r'userName'] = this.userName;
|
||||||
json[r'videos'] = this.videos;
|
json[r'videos'] = this.videos;
|
||||||
@ -82,6 +94,8 @@ class UsageByUserDto {
|
|||||||
photos: mapValueOfType<int>(json, r'photos')!,
|
photos: mapValueOfType<int>(json, r'photos')!,
|
||||||
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
||||||
usage: mapValueOfType<int>(json, r'usage')!,
|
usage: mapValueOfType<int>(json, r'usage')!,
|
||||||
|
usagePhotos: mapValueOfType<int>(json, r'usagePhotos')!,
|
||||||
|
usageVideos: mapValueOfType<int>(json, r'usageVideos')!,
|
||||||
userId: mapValueOfType<String>(json, r'userId')!,
|
userId: mapValueOfType<String>(json, r'userId')!,
|
||||||
userName: mapValueOfType<String>(json, r'userName')!,
|
userName: mapValueOfType<String>(json, r'userName')!,
|
||||||
videos: mapValueOfType<int>(json, r'videos')!,
|
videos: mapValueOfType<int>(json, r'videos')!,
|
||||||
@ -135,6 +149,8 @@ class UsageByUserDto {
|
|||||||
'photos',
|
'photos',
|
||||||
'quotaSizeInBytes',
|
'quotaSizeInBytes',
|
||||||
'usage',
|
'usage',
|
||||||
|
'usagePhotos',
|
||||||
|
'usageVideos',
|
||||||
'userId',
|
'userId',
|
||||||
'userName',
|
'userName',
|
||||||
'videos',
|
'videos',
|
||||||
|
@ -11017,7 +11017,9 @@
|
|||||||
{
|
{
|
||||||
"photos": 1,
|
"photos": 1,
|
||||||
"videos": 1,
|
"videos": 1,
|
||||||
"diskUsageRaw": 1
|
"diskUsageRaw": 2,
|
||||||
|
"usagePhotos": 1,
|
||||||
|
"usageVideos": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"items": {
|
"items": {
|
||||||
@ -11026,6 +11028,16 @@
|
|||||||
"title": "Array of usage for each user",
|
"title": "Array of usage for each user",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
|
"usagePhotos": {
|
||||||
|
"default": 0,
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"usageVideos": {
|
||||||
|
"default": 0,
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"videos": {
|
"videos": {
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
@ -11035,6 +11047,8 @@
|
|||||||
"photos",
|
"photos",
|
||||||
"usage",
|
"usage",
|
||||||
"usageByUser",
|
"usageByUser",
|
||||||
|
"usagePhotos",
|
||||||
|
"usageVideos",
|
||||||
"videos"
|
"videos"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@ -12614,6 +12628,14 @@
|
|||||||
"format": "int64",
|
"format": "int64",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"usagePhotos": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"usageVideos": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"userId": {
|
"userId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -12628,6 +12650,8 @@
|
|||||||
"photos",
|
"photos",
|
||||||
"quotaSizeInBytes",
|
"quotaSizeInBytes",
|
||||||
"usage",
|
"usage",
|
||||||
|
"usagePhotos",
|
||||||
|
"usageVideos",
|
||||||
"userId",
|
"userId",
|
||||||
"userName",
|
"userName",
|
||||||
"videos"
|
"videos"
|
||||||
|
@ -976,6 +976,8 @@ export type UsageByUserDto = {
|
|||||||
photos: number;
|
photos: number;
|
||||||
quotaSizeInBytes: number | null;
|
quotaSizeInBytes: number | null;
|
||||||
usage: number;
|
usage: number;
|
||||||
|
usagePhotos: number;
|
||||||
|
usageVideos: number;
|
||||||
userId: string;
|
userId: string;
|
||||||
userName: string;
|
userName: string;
|
||||||
videos: number;
|
videos: number;
|
||||||
@ -984,6 +986,8 @@ export type ServerStatsResponseDto = {
|
|||||||
photos: number;
|
photos: number;
|
||||||
usage: number;
|
usage: number;
|
||||||
usageByUser: UsageByUserDto[];
|
usageByUser: UsageByUserDto[];
|
||||||
|
usagePhotos: number;
|
||||||
|
usageVideos: number;
|
||||||
videos: number;
|
videos: number;
|
||||||
};
|
};
|
||||||
export type ServerStorageResponseDto = {
|
export type ServerStorageResponseDto = {
|
||||||
|
@ -86,6 +86,10 @@ export class UsageByUserDto {
|
|||||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
usage!: number;
|
usage!: number;
|
||||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
|
usagePhotos!: number;
|
||||||
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
|
usageVideos!: number;
|
||||||
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
quotaSizeInBytes!: number | null;
|
quotaSizeInBytes!: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +103,12 @@ export class ServerStatsResponseDto {
|
|||||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
usage = 0;
|
usage = 0;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
|
usagePhotos = 0;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
|
usageVideos = 0;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
isArray: true,
|
isArray: true,
|
||||||
type: UsageByUserDto,
|
type: UsageByUserDto,
|
||||||
@ -107,7 +117,9 @@ export class ServerStatsResponseDto {
|
|||||||
{
|
{
|
||||||
photos: 1,
|
photos: 1,
|
||||||
videos: 1,
|
videos: 1,
|
||||||
diskUsageRaw: 1,
|
diskUsageRaw: 2,
|
||||||
|
usagePhotos: 1,
|
||||||
|
usageVideos: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -11,6 +11,8 @@ export interface UserStatsQueryResponse {
|
|||||||
photos: number;
|
photos: number;
|
||||||
videos: number;
|
videos: number;
|
||||||
usage: number;
|
usage: number;
|
||||||
|
usagePhotos: number;
|
||||||
|
usageVideos: number;
|
||||||
quotaSizeInBytes: number | null;
|
quotaSizeInBytes: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +140,23 @@ SELECT
|
|||||||
"assets"."libraryId" IS NULL
|
"assets"."libraryId" IS NULL
|
||||||
),
|
),
|
||||||
0
|
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
|
FROM
|
||||||
"users" "users"
|
"users" "users"
|
||||||
LEFT JOIN "assets" "assets" ON "assets"."ownerId" = "users"."id"
|
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 = 'IMAGE' AND assets.isVisible)`, 'photos')
|
||||||
.addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos')
|
.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), 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')
|
.addSelect('users.quotaSizeInBytes', 'quotaSizeInBytes')
|
||||||
.leftJoin('users.assets', 'assets')
|
.leftJoin('users.assets', 'assets')
|
||||||
.leftJoin('assets.exifInfo', 'exif')
|
.leftJoin('assets.exifInfo', 'exif')
|
||||||
@ -119,6 +127,8 @@ export class UserRepository implements IUserRepository {
|
|||||||
stat.photos = Number(stat.photos);
|
stat.photos = Number(stat.photos);
|
||||||
stat.videos = Number(stat.videos);
|
stat.videos = Number(stat.videos);
|
||||||
stat.usage = Number(stat.usage);
|
stat.usage = Number(stat.usage);
|
||||||
|
stat.usagePhotos = Number(stat.usagePhotos);
|
||||||
|
stat.usageVideos = Number(stat.usageVideos);
|
||||||
stat.quotaSizeInBytes = stat.quotaSizeInBytes;
|
stat.quotaSizeInBytes = stat.quotaSizeInBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +185,8 @@ describe(ServerService.name, () => {
|
|||||||
photos: 10,
|
photos: 10,
|
||||||
videos: 11,
|
videos: 11,
|
||||||
usage: 12_345,
|
usage: 12_345,
|
||||||
|
usagePhotos: 1,
|
||||||
|
usageVideos: 11_345,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -193,6 +195,8 @@ describe(ServerService.name, () => {
|
|||||||
photos: 10,
|
photos: 10,
|
||||||
videos: 20,
|
videos: 20,
|
||||||
usage: 123_456,
|
usage: 123_456,
|
||||||
|
usagePhotos: 100,
|
||||||
|
usageVideos: 23_456,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -201,6 +205,8 @@ describe(ServerService.name, () => {
|
|||||||
photos: 100,
|
photos: 100,
|
||||||
videos: 0,
|
videos: 0,
|
||||||
usage: 987_654,
|
usage: 987_654,
|
||||||
|
usagePhotos: 900,
|
||||||
|
usageVideos: 87_654,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -209,11 +215,15 @@ describe(ServerService.name, () => {
|
|||||||
photos: 120,
|
photos: 120,
|
||||||
videos: 31,
|
videos: 31,
|
||||||
usage: 1_123_455,
|
usage: 1_123_455,
|
||||||
|
usagePhotos: 1001,
|
||||||
|
usageVideos: 122_455,
|
||||||
usageByUser: [
|
usageByUser: [
|
||||||
{
|
{
|
||||||
photos: 10,
|
photos: 10,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
usage: 12_345,
|
usage: 12_345,
|
||||||
|
usagePhotos: 1,
|
||||||
|
usageVideos: 11_345,
|
||||||
userName: '1 User',
|
userName: '1 User',
|
||||||
userId: 'user1',
|
userId: 'user1',
|
||||||
videos: 11,
|
videos: 11,
|
||||||
@ -222,6 +232,8 @@ describe(ServerService.name, () => {
|
|||||||
photos: 10,
|
photos: 10,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
usage: 123_456,
|
usage: 123_456,
|
||||||
|
usagePhotos: 100,
|
||||||
|
usageVideos: 23_456,
|
||||||
userName: '2 User',
|
userName: '2 User',
|
||||||
userId: 'user2',
|
userId: 'user2',
|
||||||
videos: 20,
|
videos: 20,
|
||||||
@ -230,6 +242,8 @@ describe(ServerService.name, () => {
|
|||||||
photos: 100,
|
photos: 100,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
usage: 987_654,
|
usage: 987_654,
|
||||||
|
usagePhotos: 900,
|
||||||
|
usageVideos: 87_654,
|
||||||
userName: '3 User',
|
userName: '3 User',
|
||||||
userId: 'user3',
|
userId: 'user3',
|
||||||
videos: 0,
|
videos: 0,
|
||||||
|
@ -126,11 +126,16 @@ export class ServerService extends BaseService {
|
|||||||
usage.photos = user.photos;
|
usage.photos = user.photos;
|
||||||
usage.videos = user.videos;
|
usage.videos = user.videos;
|
||||||
usage.usage = user.usage;
|
usage.usage = user.usage;
|
||||||
|
usage.usagePhotos = user.usagePhotos;
|
||||||
|
usage.usageVideos = user.usageVideos;
|
||||||
usage.quotaSizeInBytes = user.quotaSizeInBytes;
|
usage.quotaSizeInBytes = user.quotaSizeInBytes;
|
||||||
|
|
||||||
serverStats.photos += usage.photos;
|
serverStats.photos += usage.photos;
|
||||||
serverStats.videos += usage.videos;
|
serverStats.videos += usage.videos;
|
||||||
serverStats.usage += usage.usage;
|
serverStats.usage += usage.usage;
|
||||||
|
serverStats.usagePhotos += usage.usagePhotos;
|
||||||
|
serverStats.usageVideos += usage.usageVideos;
|
||||||
|
|
||||||
serverStats.usageByUser.push(usage);
|
serverStats.usageByUser.push(usage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
photos: 0,
|
photos: 0,
|
||||||
videos: 0,
|
videos: 0,
|
||||||
usage: 0,
|
usage: 0,
|
||||||
|
usagePhotos: 0,
|
||||||
|
usageVideos: 0,
|
||||||
usageByUser: [],
|
usageByUser: [],
|
||||||
},
|
},
|
||||||
}: Props = $props();
|
}: 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"
|
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.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"
|
||||||
<td class="w-1/4 text-ellipsis px-2 text-sm">{user.videos.toLocaleString($locale)}</td>
|
>{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">
|
<td class="w-1/4 text-ellipsis px-2 text-sm">
|
||||||
{getByteUnitString(user.usage, $locale, 0)}
|
{getByteUnitString(user.usage, $locale, 0)}
|
||||||
{#if user.quotaSizeInBytes}
|
{#if user.quotaSizeInBytes}
|
||||||
|
@ -58,7 +58,6 @@
|
|||||||
const handleToggle = (user: UserResponseDto) => {
|
const handleToggle = (user: UserResponseDto) => {
|
||||||
if (Object.keys(selectedUsers).includes(user.id)) {
|
if (Object.keys(selectedUsers).includes(user.id)) {
|
||||||
delete selectedUsers[user.id];
|
delete selectedUsers[user.id];
|
||||||
selectedUsers = selectedUsers;
|
|
||||||
} else {
|
} else {
|
||||||
selectedUsers[user.id] = { user, role: AlbumUserRole.Editor };
|
selectedUsers[user.id] = { user, role: AlbumUserRole.Editor };
|
||||||
}
|
}
|
||||||
@ -67,7 +66,6 @@
|
|||||||
const handleChangeRole = (user: UserResponseDto, role: AlbumUserRole | 'none') => {
|
const handleChangeRole = (user: UserResponseDto, role: AlbumUserRole | 'none') => {
|
||||||
if (role === 'none') {
|
if (role === 'none') {
|
||||||
delete selectedUsers[user.id];
|
delete selectedUsers[user.id];
|
||||||
selectedUsers = selectedUsers;
|
|
||||||
} else {
|
} else {
|
||||||
selectedUsers[user.id].role = role;
|
selectedUsers[user.id].role = role;
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,6 @@
|
|||||||
try {
|
try {
|
||||||
await deleteActivity({ id: reaction.id });
|
await deleteActivity({ id: reaction.id });
|
||||||
reactions.splice(index, 1);
|
reactions.splice(index, 1);
|
||||||
reactions = reactions;
|
|
||||||
if (isLiked && reaction.type === ReactionType.Like && reaction.id == isLiked.id) {
|
if (isLiked && reaction.type === ReactionType.Like && reaction.id == isLiked.id) {
|
||||||
onDeleteLike();
|
onDeleteLike();
|
||||||
} else {
|
} else {
|
||||||
@ -143,8 +142,6 @@
|
|||||||
|
|
||||||
message = '';
|
message = '';
|
||||||
onAddComment();
|
onAddComment();
|
||||||
// Re-render the activity feed
|
|
||||||
reactions = reactions;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.unable_to_add_comment'));
|
handleError(error, $t('errors.unable_to_add_comment'));
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -43,10 +43,10 @@
|
|||||||
import DetailPanel from './detail-panel.svelte';
|
import DetailPanel from './detail-panel.svelte';
|
||||||
import CropArea from './editor/crop-tool/crop-area.svelte';
|
import CropArea from './editor/crop-tool/crop-area.svelte';
|
||||||
import EditorPanel from './editor/editor-panel.svelte';
|
import EditorPanel from './editor/editor-panel.svelte';
|
||||||
import PanoramaViewer from './panorama-viewer.svelte';
|
|
||||||
import PhotoViewer from './photo-viewer.svelte';
|
import PhotoViewer from './photo-viewer.svelte';
|
||||||
import SlideshowBar from './slideshow-bar.svelte';
|
import SlideshowBar from './slideshow-bar.svelte';
|
||||||
import VideoViewer from './video-wrapper-viewer.svelte';
|
import VideoViewer from './video-wrapper-viewer.svelte';
|
||||||
|
import ImagePanoramaViewer from './image-panorama-viewer.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assetStore?: AssetStore | null;
|
assetStore?: AssetStore | null;
|
||||||
@ -512,7 +512,7 @@
|
|||||||
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath
|
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.endsWith('.insp'))}
|
.endsWith('.insp'))}
|
||||||
<PanoramaViewer {asset} />
|
<ImagePanoramaViewer {asset} />
|
||||||
{:else if isShowEditor && selectedEditType === 'crop'}
|
{:else if isShowEditor && selectedEditType === 'crop'}
|
||||||
<CropArea {asset} />
|
<CropArea {asset} />
|
||||||
{:else}
|
{: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 {
|
interface Props {
|
||||||
panorama: string | { source: string };
|
panorama: string | { source: string };
|
||||||
originalImageUrl: string | null;
|
originalImageUrl?: string;
|
||||||
adapter?: AdapterConstructor | [AdapterConstructor, unknown];
|
adapter?: AdapterConstructor | [AdapterConstructor, unknown];
|
||||||
plugins?: (PluginConstructor | [PluginConstructor, unknown])[];
|
plugins?: (PluginConstructor | [PluginConstructor, unknown])[];
|
||||||
navbar?: boolean;
|
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">
|
<script lang="ts">
|
||||||
import { AssetTypeEnum } from '@immich/sdk';
|
|
||||||
import { ProjectionType } from '$lib/constants';
|
import { ProjectionType } from '$lib/constants';
|
||||||
import VideoNativeViewer from '$lib/components/asset-viewer/video-native-viewer.svelte';
|
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 {
|
interface Props {
|
||||||
assetId: string;
|
assetId: string;
|
||||||
@ -30,7 +29,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if projectionType === ProjectionType.EQUIRECTANGULAR}
|
{#if projectionType === ProjectionType.EQUIRECTANGULAR}
|
||||||
<PanoramaViewer asset={{ id: assetId, type: AssetTypeEnum.Video }} />
|
<VideoPanoramaViewer {assetId} />
|
||||||
{:else}
|
{:else}
|
||||||
<VideoNativeViewer
|
<VideoNativeViewer
|
||||||
{loopVideo}
|
{loopVideo}
|
||||||
|
@ -90,7 +90,6 @@
|
|||||||
for (const person of people) {
|
for (const person of people) {
|
||||||
person.isHidden = personIsHidden[person.id];
|
person.isHidden = personIsHidden[person.id];
|
||||||
}
|
}
|
||||||
people = people;
|
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -101,15 +101,9 @@
|
|||||||
const handleReset = (id: string) => {
|
const handleReset = (id: string) => {
|
||||||
if (selectedPersonToReassign[id]) {
|
if (selectedPersonToReassign[id]) {
|
||||||
delete selectedPersonToReassign[id];
|
delete selectedPersonToReassign[id];
|
||||||
|
|
||||||
// trigger reactivity
|
|
||||||
selectedPersonToReassign = selectedPersonToReassign;
|
|
||||||
}
|
}
|
||||||
if (selectedPersonToCreate[id]) {
|
if (selectedPersonToCreate[id]) {
|
||||||
delete selectedPersonToCreate[id];
|
delete selectedPersonToCreate[id];
|
||||||
|
|
||||||
// trigger reactivity
|
|
||||||
selectedPersonToCreate = selectedPersonToCreate;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -35,12 +35,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectedIds.add(option.value);
|
selectedIds.add(option.value);
|
||||||
selectedIds = selectedIds;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = (tag: string) => {
|
const handleRemove = (tag: string) => {
|
||||||
selectedIds.delete(tag);
|
selectedIds.delete(tag);
|
||||||
selectedIds = selectedIds;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onsubmit = (event: Event) => {
|
const onsubmit = (event: Event) => {
|
||||||
|
@ -112,7 +112,6 @@
|
|||||||
assets.findIndex((a) => a.id === action.asset.id),
|
assets.findIndex((a) => a.id === action.asset.id),
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
assets = assets;
|
|
||||||
if (assets.length === 0) {
|
if (assets.length === 0) {
|
||||||
await goto(AppRoute.PHOTOS);
|
await goto(AppRoute.PHOTOS);
|
||||||
} else if (currentViewAssetIndex === assets.length) {
|
} else if (currentViewAssetIndex === assets.length) {
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
export type SearchFilter = {
|
export type SearchFilter = {
|
||||||
query: string;
|
query: string;
|
||||||
queryType: 'smart' | 'metadata';
|
queryType: 'smart' | 'metadata';
|
||||||
personIds: Set<string>;
|
personIds: SvelteSet<string>;
|
||||||
location: SearchLocationFilter;
|
location: SearchLocationFilter;
|
||||||
camera: SearchCameraFilter;
|
camera: SearchCameraFilter;
|
||||||
date: SearchDateFilter;
|
date: SearchDateFilter;
|
||||||
|
@ -9,9 +9,10 @@
|
|||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte';
|
import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte';
|
||||||
|
import type { SvelteSet } from 'svelte/reactivity';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedPeople: Set<string>;
|
selectedPeople: SvelteSet<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { selectedPeople = $bindable() }: Props = $props();
|
let { selectedPeople = $bindable() }: Props = $props();
|
||||||
@ -43,7 +44,6 @@
|
|||||||
} else {
|
} else {
|
||||||
selectedPeople.add(id);
|
selectedPeople.add(id);
|
||||||
}
|
}
|
||||||
selectedPeople = selectedPeople;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterPeople = (list: PersonResponseDto[], name: string) => {
|
const filterPeople = (list: PersonResponseDto[], name: string) => {
|
||||||
|
@ -117,7 +117,6 @@
|
|||||||
await updatePartner({ id: partner.user.id, updatePartnerDto: { inTimeline } });
|
await updatePartner({ id: partner.user.id, updatePartnerDto: { inTimeline } });
|
||||||
|
|
||||||
partner.inTimeline = inTimeline;
|
partner.inTimeline = inTimeline;
|
||||||
partners = partners;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.unable_to_update_timeline_display_status'));
|
handleError(error, $t('errors.unable_to_update_timeline_display_status'));
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectedAssetIds.add(suggestedAsset.id);
|
selectedAssetIds.add(suggestedAsset.id);
|
||||||
selectedAssetIds = selectedAssetIds;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
@ -48,13 +47,10 @@
|
|||||||
} else {
|
} else {
|
||||||
selectedAssetIds.add(asset.id);
|
selectedAssetIds.add(asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedAssetIds = selectedAssetIds;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectNone = () => {
|
const onSelectNone = () => {
|
||||||
selectedAssetIds.clear();
|
selectedAssetIds.clear();
|
||||||
selectedAssetIds = selectedAssetIds;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectAll = () => {
|
const onSelectAll = () => {
|
||||||
|
@ -35,8 +35,6 @@
|
|||||||
person.updatedAt = Date.now().toString();
|
person.updatedAt = Date.now().toString();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// trigger reactivity
|
|
||||||
people = people;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -74,9 +74,6 @@
|
|||||||
person.updatedAt = new Date().toISOString();
|
person.updatedAt = new Date().toISOString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// trigger reactivity
|
|
||||||
people = people;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -146,9 +143,6 @@
|
|||||||
message: $t('change_name_successfully'),
|
message: $t('change_name_successfully'),
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
|
|
||||||
// trigger reactivity
|
|
||||||
people = people;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.unable_to_save_name'));
|
handleError(error, $t('errors.unable_to_save_name'));
|
||||||
}
|
}
|
||||||
|
@ -156,8 +156,6 @@
|
|||||||
|
|
||||||
searchResultAlbums.push(...albums.items);
|
searchResultAlbums.push(...albums.items);
|
||||||
searchResultAssets.push(...assets.items);
|
searchResultAssets.push(...assets.items);
|
||||||
searchResultAlbums = searchResultAlbums;
|
|
||||||
searchResultAssets = searchResultAssets;
|
|
||||||
|
|
||||||
nextPage = assets.nextPage ? Number(assets.nextPage) : null;
|
nextPage = assets.nextPage ? Number(assets.nextPage) : null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Reference in New Issue
Block a user