mirror of
https://github.com/immich-app/immich.git
synced 2026-01-20 08:40:53 -08:00
Compare commits
14 Commits
refactor/a
...
fix/cloud-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bde330e777 | ||
|
|
fe1d0edf4c | ||
|
|
4ef699e9fa | ||
|
|
3e21174dd8 | ||
|
|
1b56bb84f9 | ||
|
|
b3f5b8ede8 | ||
|
|
2b77dc8e1f | ||
|
|
97a594556b | ||
|
|
4a7c4b6d15 | ||
|
|
a8198f9934 | ||
|
|
b123beae38 | ||
|
|
1ada7a8340 | ||
|
|
d18f385653 | ||
|
|
61ed77a29f |
@@ -603,7 +603,7 @@
|
||||
"backup_album_selection_page_select_albums": "Select albums",
|
||||
"backup_album_selection_page_selection_info": "Selection Info",
|
||||
"backup_album_selection_page_total_assets": "Total unique assets",
|
||||
"backup_albums_sync": "Backup albums synchronization",
|
||||
"backup_albums_sync": "Backup Albums Synchronization",
|
||||
"backup_all": "All",
|
||||
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
|
||||
"backup_background_service_complete_notification": "Asset backup complete",
|
||||
@@ -2257,7 +2257,7 @@
|
||||
"url": "URL",
|
||||
"usage": "Usage",
|
||||
"use_biometric": "Use biometric",
|
||||
"use_current_connection": "use current connection",
|
||||
"use_current_connection": "Use current connection",
|
||||
"use_custom_date_range": "Use custom date range instead",
|
||||
"user": "User",
|
||||
"user_has_been_deleted": "This user has been deleted.",
|
||||
|
||||
@@ -117,6 +117,9 @@
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:pathPrefix="/memories/" />
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:path="/memory" />
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:pathPrefix="/photos/" />
|
||||
|
||||
1
mobile/drift_schemas/main/drift_schema_v17.json
generated
Normal file
1
mobile/drift_schemas/main/drift_schema_v17.json
generated
Normal file
File diff suppressed because one or more lines are too long
@@ -22,6 +22,7 @@ sealed class BaseAsset {
|
||||
final int? durationInSeconds;
|
||||
final bool isFavorite;
|
||||
final String? livePhotoVideoId;
|
||||
final bool isEdited;
|
||||
|
||||
const BaseAsset({
|
||||
required this.name,
|
||||
@@ -34,6 +35,7 @@ sealed class BaseAsset {
|
||||
this.durationInSeconds,
|
||||
this.isFavorite = false,
|
||||
this.livePhotoVideoId,
|
||||
required this.isEdited,
|
||||
});
|
||||
|
||||
bool get isImage => type == AssetType.image;
|
||||
@@ -71,6 +73,7 @@ sealed class BaseAsset {
|
||||
height: ${height ?? "<NA>"},
|
||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||
isFavorite: $isFavorite,
|
||||
isEdited: $isEdited,
|
||||
}''';
|
||||
}
|
||||
|
||||
@@ -85,7 +88,8 @@ sealed class BaseAsset {
|
||||
width == other.width &&
|
||||
height == other.height &&
|
||||
durationInSeconds == other.durationInSeconds &&
|
||||
isFavorite == other.isFavorite;
|
||||
isFavorite == other.isFavorite &&
|
||||
isEdited == other.isEdited;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -99,6 +103,7 @@ sealed class BaseAsset {
|
||||
width.hashCode ^
|
||||
height.hashCode ^
|
||||
durationInSeconds.hashCode ^
|
||||
isFavorite.hashCode;
|
||||
isFavorite.hashCode ^
|
||||
isEdited.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class LocalAsset extends BaseAsset {
|
||||
this.adjustmentTime,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
required super.isEdited,
|
||||
}) : remoteAssetId = remoteId;
|
||||
|
||||
@override
|
||||
@@ -107,6 +108,7 @@ class LocalAsset extends BaseAsset {
|
||||
DateTime? adjustmentTime,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
bool? isEdited,
|
||||
}) {
|
||||
return LocalAsset(
|
||||
id: id ?? this.id,
|
||||
@@ -125,6 +127,7 @@ class LocalAsset extends BaseAsset {
|
||||
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
isEdited: isEdited ?? this.isEdited,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class RemoteAsset extends BaseAsset {
|
||||
this.visibility = AssetVisibility.timeline,
|
||||
super.livePhotoVideoId,
|
||||
this.stackId,
|
||||
required super.isEdited,
|
||||
}) : localAssetId = localId;
|
||||
|
||||
@override
|
||||
@@ -104,6 +105,7 @@ class RemoteAsset extends BaseAsset {
|
||||
AssetVisibility? visibility,
|
||||
String? livePhotoVideoId,
|
||||
String? stackId,
|
||||
bool? isEdited,
|
||||
}) {
|
||||
return RemoteAsset(
|
||||
id: id ?? this.id,
|
||||
@@ -122,6 +124,7 @@ class RemoteAsset extends BaseAsset {
|
||||
visibility: visibility ?? this.visibility,
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
stackId: stackId ?? this.stackId,
|
||||
isEdited: isEdited ?? this.isEdited,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,5 +436,6 @@ extension PlatformToLocalAsset on PlatformAsset {
|
||||
adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true),
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ extension on AssetResponseDto {
|
||||
thumbHash: thumbhash,
|
||||
localId: null,
|
||||
type: type.toAssetType(),
|
||||
isEdited: isEdited,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +247,42 @@ class SyncStreamService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleWsAssetEditReadyV1Batch(List<dynamic> batchData) async {
|
||||
if (batchData.isEmpty) return;
|
||||
|
||||
_logger.info('Processing batch of ${batchData.length} AssetEditReadyV1 events');
|
||||
|
||||
final List<SyncAssetV1> assets = [];
|
||||
|
||||
try {
|
||||
for (final data in batchData) {
|
||||
if (data is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final payload = data;
|
||||
final assetData = payload['asset'];
|
||||
|
||||
if (assetData == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final asset = SyncAssetV1.fromJson(assetData);
|
||||
|
||||
if (asset != null) {
|
||||
assets.add(asset);
|
||||
}
|
||||
}
|
||||
|
||||
if (assets.isNotEmpty) {
|
||||
await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-edit');
|
||||
_logger.info('Successfully processed ${assets.length} edited assets');
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe("Error processing AssetEditReadyV1 websocket batch events", error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleRemoteTrashed(Iterable<String> checksums) async {
|
||||
if (checksums.isEmpty) {
|
||||
return Future.value();
|
||||
|
||||
@@ -196,6 +196,16 @@ class BackgroundSyncManager {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncWebsocketEditBatch(List<dynamic> batchData) {
|
||||
if (_syncWebsocketTask != null) {
|
||||
return _syncWebsocketTask!.future;
|
||||
}
|
||||
_syncWebsocketTask = _handleWsAssetEditReadyV1Batch(batchData);
|
||||
return _syncWebsocketTask!.whenComplete(() {
|
||||
_syncWebsocketTask = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncLinkedAlbum() {
|
||||
if (_linkedAlbumSyncTask != null) {
|
||||
return _linkedAlbumSyncTask!.future;
|
||||
@@ -231,3 +241,8 @@ Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => ru
|
||||
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData),
|
||||
debugLabel: 'websocket-batch',
|
||||
);
|
||||
|
||||
Cancelable<void> _handleWsAssetEditReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
|
||||
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV1Batch(batchData),
|
||||
debugLabel: 'websocket-edit',
|
||||
);
|
||||
|
||||
@@ -47,5 +47,6 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
cloudId: iCloudId,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ SELECT
|
||||
NULL as i_cloud_id,
|
||||
NULL as latitude,
|
||||
NULL as longitude,
|
||||
NULL as adjustmentTime
|
||||
NULL as adjustmentTime,
|
||||
rae.is_edited
|
||||
FROM
|
||||
remote_asset_entity rae
|
||||
LEFT JOIN
|
||||
@@ -61,7 +62,8 @@ SELECT
|
||||
lae.i_cloud_id,
|
||||
lae.latitude,
|
||||
lae.longitude,
|
||||
lae.adjustment_time
|
||||
lae.adjustment_time,
|
||||
0 as is_edited
|
||||
FROM
|
||||
local_asset_entity lae
|
||||
WHERE NOT EXISTS (
|
||||
|
||||
@@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
);
|
||||
$arrayStartIndex += generatedlimit.amountOfVariables;
|
||||
return customSelect(
|
||||
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
|
||||
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
|
||||
variables: [
|
||||
for (var $ in userIds) i0.Variable<String>($),
|
||||
...generatedlimit.introducedVariables,
|
||||
@@ -66,6 +66,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
latitude: row.readNullable<double>('latitude'),
|
||||
longitude: row.readNullable<double>('longitude'),
|
||||
adjustmentTime: row.readNullable<DateTime>('adjustmentTime'),
|
||||
isEdited: row.read<bool>('is_edited'),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -137,6 +138,7 @@ class MergedAssetResult {
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final DateTime? adjustmentTime;
|
||||
final bool isEdited;
|
||||
MergedAssetResult({
|
||||
this.remoteId,
|
||||
this.localId,
|
||||
@@ -158,6 +160,7 @@ class MergedAssetResult {
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.adjustmentTime,
|
||||
required this.isEdited,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin
|
||||
|
||||
TextColumn get libraryId => text().nullable()();
|
||||
|
||||
BoolColumn get isEdited => boolean().withDefault(const Constant(false))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
@@ -66,5 +68,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
||||
livePhotoVideoId: livePhotoVideoId,
|
||||
localId: localId,
|
||||
stackId: stackId,
|
||||
isEdited: isEdited,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder =
|
||||
required i2.AssetVisibility visibility,
|
||||
i0.Value<String?> stackId,
|
||||
i0.Value<String?> libraryId,
|
||||
i0.Value<bool> isEdited,
|
||||
});
|
||||
typedef $$RemoteAssetEntityTableUpdateCompanionBuilder =
|
||||
i1.RemoteAssetEntityCompanion Function({
|
||||
@@ -52,6 +53,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder =
|
||||
i0.Value<i2.AssetVisibility> visibility,
|
||||
i0.Value<String?> stackId,
|
||||
i0.Value<String?> libraryId,
|
||||
i0.Value<bool> isEdited,
|
||||
});
|
||||
|
||||
final class $$RemoteAssetEntityTableReferences
|
||||
@@ -196,6 +198,11 @@ class $$RemoteAssetEntityTableFilterComposer
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<bool> get isEdited => $composableBuilder(
|
||||
column: $table.isEdited,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i5.$$UserEntityTableFilterComposer get ownerId {
|
||||
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
@@ -318,6 +325,11 @@ class $$RemoteAssetEntityTableOrderingComposer
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<bool> get isEdited => $composableBuilder(
|
||||
column: $table.isEdited,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i5.$$UserEntityTableOrderingComposer get ownerId {
|
||||
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
@@ -417,6 +429,9 @@ class $$RemoteAssetEntityTableAnnotationComposer
|
||||
i0.GeneratedColumn<String> get libraryId =>
|
||||
$composableBuilder(column: $table.libraryId, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<bool> get isEdited =>
|
||||
$composableBuilder(column: $table.isEdited, builder: (column) => column);
|
||||
|
||||
i5.$$UserEntityTableAnnotationComposer get ownerId {
|
||||
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
@@ -497,6 +512,7 @@ class $$RemoteAssetEntityTableTableManager
|
||||
const i0.Value.absent(),
|
||||
i0.Value<String?> stackId = const i0.Value.absent(),
|
||||
i0.Value<String?> libraryId = const i0.Value.absent(),
|
||||
i0.Value<bool> isEdited = const i0.Value.absent(),
|
||||
}) => i1.RemoteAssetEntityCompanion(
|
||||
name: name,
|
||||
type: type,
|
||||
@@ -516,6 +532,7 @@ class $$RemoteAssetEntityTableTableManager
|
||||
visibility: visibility,
|
||||
stackId: stackId,
|
||||
libraryId: libraryId,
|
||||
isEdited: isEdited,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
@@ -537,6 +554,7 @@ class $$RemoteAssetEntityTableTableManager
|
||||
required i2.AssetVisibility visibility,
|
||||
i0.Value<String?> stackId = const i0.Value.absent(),
|
||||
i0.Value<String?> libraryId = const i0.Value.absent(),
|
||||
i0.Value<bool> isEdited = const i0.Value.absent(),
|
||||
}) => i1.RemoteAssetEntityCompanion.insert(
|
||||
name: name,
|
||||
type: type,
|
||||
@@ -556,6 +574,7 @@ class $$RemoteAssetEntityTableTableManager
|
||||
visibility: visibility,
|
||||
stackId: stackId,
|
||||
libraryId: libraryId,
|
||||
isEdited: isEdited,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map(
|
||||
@@ -844,6 +863,21 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const i0.VerificationMeta _isEditedMeta = const i0.VerificationMeta(
|
||||
'isEdited',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<bool> isEdited = i0.GeneratedColumn<bool>(
|
||||
'is_edited',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.bool,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_edited" IN (0, 1))',
|
||||
),
|
||||
defaultValue: const i4.Constant(false),
|
||||
);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [
|
||||
name,
|
||||
@@ -864,6 +898,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
||||
visibility,
|
||||
stackId,
|
||||
libraryId,
|
||||
isEdited,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@@ -987,6 +1022,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
||||
libraryId.isAcceptableOrUnknown(data['library_id']!, _libraryIdMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('is_edited')) {
|
||||
context.handle(
|
||||
_isEditedMeta,
|
||||
isEdited.isAcceptableOrUnknown(data['is_edited']!, _isEditedMeta),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -1075,6 +1116,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}library_id'],
|
||||
),
|
||||
isEdited: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.bool,
|
||||
data['${effectivePrefix}is_edited'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1115,6 +1160,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
final i2.AssetVisibility visibility;
|
||||
final String? stackId;
|
||||
final String? libraryId;
|
||||
final bool isEdited;
|
||||
const RemoteAssetEntityData({
|
||||
required this.name,
|
||||
required this.type,
|
||||
@@ -1134,6 +1180,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
required this.visibility,
|
||||
this.stackId,
|
||||
this.libraryId,
|
||||
required this.isEdited,
|
||||
});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
@@ -1182,6 +1229,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
if (!nullToAbsent || libraryId != null) {
|
||||
map['library_id'] = i0.Variable<String>(libraryId);
|
||||
}
|
||||
map['is_edited'] = i0.Variable<bool>(isEdited);
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1213,6 +1261,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
),
|
||||
stackId: serializer.fromJson<String?>(json['stackId']),
|
||||
libraryId: serializer.fromJson<String?>(json['libraryId']),
|
||||
isEdited: serializer.fromJson<bool>(json['isEdited']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@@ -1241,6 +1290,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
),
|
||||
'stackId': serializer.toJson<String?>(stackId),
|
||||
'libraryId': serializer.toJson<String?>(libraryId),
|
||||
'isEdited': serializer.toJson<bool>(isEdited),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1263,6 +1313,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
i2.AssetVisibility? visibility,
|
||||
i0.Value<String?> stackId = const i0.Value.absent(),
|
||||
i0.Value<String?> libraryId = const i0.Value.absent(),
|
||||
bool? isEdited,
|
||||
}) => i1.RemoteAssetEntityData(
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
@@ -1288,6 +1339,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
visibility: visibility ?? this.visibility,
|
||||
stackId: stackId.present ? stackId.value : this.stackId,
|
||||
libraryId: libraryId.present ? libraryId.value : this.libraryId,
|
||||
isEdited: isEdited ?? this.isEdited,
|
||||
);
|
||||
RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) {
|
||||
return RemoteAssetEntityData(
|
||||
@@ -1319,6 +1371,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
: this.visibility,
|
||||
stackId: data.stackId.present ? data.stackId.value : this.stackId,
|
||||
libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId,
|
||||
isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1342,7 +1395,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
||||
..write('visibility: $visibility, ')
|
||||
..write('stackId: $stackId, ')
|
||||
..write('libraryId: $libraryId')
|
||||
..write('libraryId: $libraryId, ')
|
||||
..write('isEdited: $isEdited')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
@@ -1367,6 +1421,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
visibility,
|
||||
stackId,
|
||||
libraryId,
|
||||
isEdited,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -1389,7 +1444,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
other.livePhotoVideoId == this.livePhotoVideoId &&
|
||||
other.visibility == this.visibility &&
|
||||
other.stackId == this.stackId &&
|
||||
other.libraryId == this.libraryId);
|
||||
other.libraryId == this.libraryId &&
|
||||
other.isEdited == this.isEdited);
|
||||
}
|
||||
|
||||
class RemoteAssetEntityCompanion
|
||||
@@ -1412,6 +1468,7 @@ class RemoteAssetEntityCompanion
|
||||
final i0.Value<i2.AssetVisibility> visibility;
|
||||
final i0.Value<String?> stackId;
|
||||
final i0.Value<String?> libraryId;
|
||||
final i0.Value<bool> isEdited;
|
||||
const RemoteAssetEntityCompanion({
|
||||
this.name = const i0.Value.absent(),
|
||||
this.type = const i0.Value.absent(),
|
||||
@@ -1431,6 +1488,7 @@ class RemoteAssetEntityCompanion
|
||||
this.visibility = const i0.Value.absent(),
|
||||
this.stackId = const i0.Value.absent(),
|
||||
this.libraryId = const i0.Value.absent(),
|
||||
this.isEdited = const i0.Value.absent(),
|
||||
});
|
||||
RemoteAssetEntityCompanion.insert({
|
||||
required String name,
|
||||
@@ -1451,6 +1509,7 @@ class RemoteAssetEntityCompanion
|
||||
required i2.AssetVisibility visibility,
|
||||
this.stackId = const i0.Value.absent(),
|
||||
this.libraryId = const i0.Value.absent(),
|
||||
this.isEdited = const i0.Value.absent(),
|
||||
}) : name = i0.Value(name),
|
||||
type = i0.Value(type),
|
||||
id = i0.Value(id),
|
||||
@@ -1476,6 +1535,7 @@ class RemoteAssetEntityCompanion
|
||||
i0.Expression<int>? visibility,
|
||||
i0.Expression<String>? stackId,
|
||||
i0.Expression<String>? libraryId,
|
||||
i0.Expression<bool>? isEdited,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (name != null) 'name': name,
|
||||
@@ -1496,6 +1556,7 @@ class RemoteAssetEntityCompanion
|
||||
if (visibility != null) 'visibility': visibility,
|
||||
if (stackId != null) 'stack_id': stackId,
|
||||
if (libraryId != null) 'library_id': libraryId,
|
||||
if (isEdited != null) 'is_edited': isEdited,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1518,6 +1579,7 @@ class RemoteAssetEntityCompanion
|
||||
i0.Value<i2.AssetVisibility>? visibility,
|
||||
i0.Value<String?>? stackId,
|
||||
i0.Value<String?>? libraryId,
|
||||
i0.Value<bool>? isEdited,
|
||||
}) {
|
||||
return i1.RemoteAssetEntityCompanion(
|
||||
name: name ?? this.name,
|
||||
@@ -1538,6 +1600,7 @@ class RemoteAssetEntityCompanion
|
||||
visibility: visibility ?? this.visibility,
|
||||
stackId: stackId ?? this.stackId,
|
||||
libraryId: libraryId ?? this.libraryId,
|
||||
isEdited: isEdited ?? this.isEdited,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1602,6 +1665,9 @@ class RemoteAssetEntityCompanion
|
||||
if (libraryId.present) {
|
||||
map['library_id'] = i0.Variable<String>(libraryId.value);
|
||||
}
|
||||
if (isEdited.present) {
|
||||
map['is_edited'] = i0.Variable<bool>(isEdited.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1625,7 +1691,8 @@ class RemoteAssetEntityCompanion
|
||||
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
||||
..write('visibility: $visibility, ')
|
||||
..write('stackId: $stackId, ')
|
||||
..write('libraryId: $libraryId')
|
||||
..write('libraryId: $libraryId, ')
|
||||
..write('isEdited: $isEdited')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@@ -45,5 +45,6 @@ extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityD
|
||||
height: height,
|
||||
width: width,
|
||||
orientation: orientation,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 16;
|
||||
int get schemaVersion => 17;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -201,6 +201,9 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
await m.createIndex(v16.idxLocalAssetCloudId);
|
||||
await m.createTable(v16.remoteAssetCloudIdEntity);
|
||||
},
|
||||
from16To17: (m, v17) async {
|
||||
await m.addColumn(v17.remoteAssetEntity, v17.remoteAssetEntity.isEdited);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -6911,6 +6911,503 @@ i1.GeneratedColumn<DateTime> _column_100(String aliasedName) =>
|
||||
true,
|
||||
type: i1.DriftSqlType.dateTime,
|
||||
);
|
||||
|
||||
final class Schema17 extends i0.VersionedSchema {
|
||||
Schema17({required super.database}) : super(version: 17);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
idxLocalAssetChecksum,
|
||||
idxLocalAssetCloudId,
|
||||
idxRemoteAssetOwnerChecksum,
|
||||
uQRemoteAssetsOwnerChecksum,
|
||||
uQRemoteAssetsOwnerLibraryChecksum,
|
||||
idxRemoteAssetChecksum,
|
||||
authUserEntity,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
remoteExifEntity,
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
remoteAssetCloudIdEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity,
|
||||
personEntity,
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
idxLatLng,
|
||||
idxTrashedLocalAssetChecksum,
|
||||
idxTrashedLocalAssetAlbum,
|
||||
];
|
||||
late final Shape20 userEntity = Shape20(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_3,
|
||||
_column_84,
|
||||
_column_85,
|
||||
_column_91,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape28 remoteAssetEntity = Shape28(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_1,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_0,
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_15,
|
||||
_column_16,
|
||||
_column_17,
|
||||
_column_18,
|
||||
_column_19,
|
||||
_column_20,
|
||||
_column_21,
|
||||
_column_86,
|
||||
_column_101,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape3 stackEntity = Shape3(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'stack_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape26 localAssetEntity = Shape26(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_1,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_0,
|
||||
_column_22,
|
||||
_column_14,
|
||||
_column_23,
|
||||
_column_98,
|
||||
_column_96,
|
||||
_column_46,
|
||||
_column_47,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape9 remoteAlbumEntity = Shape9(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_56,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_15,
|
||||
_column_57,
|
||||
_column_58,
|
||||
_column_59,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape19 localAlbumEntity = Shape19(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_5,
|
||||
_column_31,
|
||||
_column_32,
|
||||
_column_90,
|
||||
_column_33,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape22 localAlbumAssetEntity = Shape22(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_34, _column_35, _column_33],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||
'idx_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxLocalAssetCloudId = i1.Index(
|
||||
'idx_local_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
|
||||
'idx_remote_asset_owner_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_library_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||
'idx_remote_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||
);
|
||||
late final Shape21 authUserEntity = Shape21(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'auth_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_3,
|
||||
_column_2,
|
||||
_column_84,
|
||||
_column_85,
|
||||
_column_92,
|
||||
_column_93,
|
||||
_column_7,
|
||||
_column_94,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape4 userMetadataEntity = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_metadata_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||
columns: [_column_25, _column_26, _column_27],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape5 partnerEntity = Shape5(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'partner_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||
columns: [_column_28, _column_29, _column_30],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape8 remoteExifEntity = Shape8(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_exif_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_36,
|
||||
_column_37,
|
||||
_column_38,
|
||||
_column_39,
|
||||
_column_40,
|
||||
_column_41,
|
||||
_column_11,
|
||||
_column_10,
|
||||
_column_42,
|
||||
_column_43,
|
||||
_column_44,
|
||||
_column_45,
|
||||
_column_46,
|
||||
_column_47,
|
||||
_column_48,
|
||||
_column_49,
|
||||
_column_50,
|
||||
_column_51,
|
||||
_column_52,
|
||||
_column_53,
|
||||
_column_54,
|
||||
_column_55,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_36, _column_60],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||
columns: [_column_60, _column_25, _column_61],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape27 remoteAssetCloudIdEntity = Shape27(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_cloud_id_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_36,
|
||||
_column_99,
|
||||
_column_100,
|
||||
_column_96,
|
||||
_column_46,
|
||||
_column_47,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape11 memoryEntity = Shape11(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_18,
|
||||
_column_15,
|
||||
_column_8,
|
||||
_column_62,
|
||||
_column_63,
|
||||
_column_64,
|
||||
_column_65,
|
||||
_column_66,
|
||||
_column_67,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape12 memoryAssetEntity = Shape12(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||
columns: [_column_36, _column_68],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape14 personEntity = Shape14(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'person_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_15,
|
||||
_column_1,
|
||||
_column_69,
|
||||
_column_71,
|
||||
_column_72,
|
||||
_column_73,
|
||||
_column_74,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape15 assetFaceEntity = Shape15(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_face_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_36,
|
||||
_column_76,
|
||||
_column_77,
|
||||
_column_78,
|
||||
_column_79,
|
||||
_column_80,
|
||||
_column_81,
|
||||
_column_82,
|
||||
_column_83,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape18 storeEntity = Shape18(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'store_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_87, _column_88, _column_89],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape25 trashedLocalAssetEntity = Shape25(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'trashed_local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||
columns: [
|
||||
_column_1,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_0,
|
||||
_column_95,
|
||||
_column_22,
|
||||
_column_14,
|
||||
_column_23,
|
||||
_column_97,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxLatLng = i1.Index(
|
||||
'idx_lat_lng',
|
||||
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||
'idx_trashed_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||
'idx_trashed_local_asset_album',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||
);
|
||||
}
|
||||
|
||||
class Shape28 extends i0.VersionedTable {
|
||||
Shape28({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get name =>
|
||||
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get type =>
|
||||
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<int> get width =>
|
||||
columnsByName['width']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get height =>
|
||||
columnsByName['height']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get durationInSeconds =>
|
||||
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get checksum =>
|
||||
columnsByName['checksum']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get isFavorite =>
|
||||
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<String> get ownerId =>
|
||||
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get localDateTime =>
|
||||
columnsByName['local_date_time']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<String> get thumbHash =>
|
||||
columnsByName['thumb_hash']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<String> get livePhotoVideoId =>
|
||||
columnsByName['live_photo_video_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get visibility =>
|
||||
columnsByName['visibility']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get stackId =>
|
||||
columnsByName['stack_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get libraryId =>
|
||||
columnsByName['library_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get isEdited =>
|
||||
columnsByName['is_edited']! as i1.GeneratedColumn<bool>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<bool> _column_101(String aliasedName) =>
|
||||
i1.GeneratedColumn<bool>(
|
||||
'is_edited',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.bool,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_edited" IN (0, 1))',
|
||||
),
|
||||
defaultValue: const CustomExpression('0'),
|
||||
);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
@@ -6927,6 +7424,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
||||
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
||||
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
@@ -7005,6 +7503,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from15To16(migrator, schema);
|
||||
return 16;
|
||||
case 16:
|
||||
final schema = Schema17(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from16To17(migrator, schema);
|
||||
return 17;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@@ -7027,6 +7530,7 @@ i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
||||
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
||||
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
@@ -7044,5 +7548,6 @@ i1.OnUpgrade stepByStep({
|
||||
from13To14: from13To14,
|
||||
from14To15: from14To15,
|
||||
from15To16: from15To16,
|
||||
from16To17: from16To17,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -200,6 +200,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
libraryId: Value(asset.libraryId),
|
||||
width: Value(asset.width),
|
||||
height: Value(asset.height),
|
||||
isEdited: Value(asset.isEdited),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
|
||||
@@ -70,6 +70,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
durationInSeconds: row.durationInSeconds,
|
||||
livePhotoVideoId: row.livePhotoVideoId,
|
||||
stackId: row.stackId,
|
||||
isEdited: row.isEdited,
|
||||
)
|
||||
: LocalAsset(
|
||||
id: row.localId!,
|
||||
@@ -88,6 +89,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
latitude: row.latitude,
|
||||
longitude: row.longitude,
|
||||
adjustmentTime: row.adjustmentTime,
|
||||
isEdited: row.isEdited,
|
||||
),
|
||||
)
|
||||
.get();
|
||||
|
||||
@@ -92,7 +92,7 @@ class _MobileLayout extends StatelessWidget {
|
||||
],
|
||||
)
|
||||
.toList();
|
||||
return ListView(padding: const EdgeInsets.only(top: 10.0, bottom: 16), children: [...settings]);
|
||||
return ListView(padding: const EdgeInsets.only(top: 10.0, bottom: 60), children: [...settings]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ class SettingsSubPage extends StatelessWidget {
|
||||
context.locale;
|
||||
return Scaffold(
|
||||
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
|
||||
body: section.widget,
|
||||
body: Padding(padding: const EdgeInsets.only(bottom: 60.0), child: section.widget),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -49,6 +50,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
final accessToken = Store.tryGet(StoreKey.accessToken);
|
||||
|
||||
if (accessToken != null && serverUrl != null && endpoint != null) {
|
||||
final infoProvider = ref.read(serverInfoProvider.notifier);
|
||||
final wsProvider = ref.read(websocketProvider.notifier);
|
||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
||||
final backupProvider = ref.read(driftBackupProvider.notifier);
|
||||
@@ -58,6 +60,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
(_) async {
|
||||
try {
|
||||
wsProvider.connect();
|
||||
unawaited(infoProvider.getServerInfo());
|
||||
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
bool syncSuccess = false;
|
||||
|
||||
@@ -18,6 +18,7 @@ class SyncStatusPage extends StatelessWidget {
|
||||
splashRadius: 24,
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
centerTitle: false,
|
||||
),
|
||||
body: const SyncStatusAndActions(),
|
||||
);
|
||||
|
||||
@@ -118,6 +118,7 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
|
||||
),
|
||||
_PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()),
|
||||
_PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
|
||||
_PropertyItem(label: 'Is Edited', value: asset.isEdited.toString()),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ class _PlaceTile extends StatelessWidget {
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: Thumbnail.remote(remoteId: place.$2, fit: BoxFit.cover),
|
||||
child: Thumbnail.remote(remoteId: place.$2, fit: BoxFit.cover, thumbhash: ""),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -14,14 +14,15 @@ import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/album_filter.utils.dart';
|
||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
@@ -310,18 +311,17 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
||||
: const Icon(Icons.abc, color: Colors.transparent),
|
||||
onPressed: () => onMenuTapped(sortMode),
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(16, 16, 32, 16)),
|
||||
padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(12, 12, 24, 12)),
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
albumSortOption == sortMode ? context.colorScheme.primary : Colors.transparent,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))),
|
||||
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
sortMode.label.t(context: context),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: albumSortOption == sortMode
|
||||
? context.colorScheme.onPrimary
|
||||
: context.colorScheme.onSurface.withAlpha(185),
|
||||
@@ -344,15 +344,12 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: albumSortIsReverse
|
||||
? const Icon(Icons.keyboard_arrow_down)
|
||||
: const Icon(Icons.keyboard_arrow_up_rounded),
|
||||
? Icon(Icons.keyboard_arrow_down, color: context.colorScheme.onSurface)
|
||||
: Icon(Icons.keyboard_arrow_up_rounded, color: context.colorScheme.onSurface),
|
||||
),
|
||||
Text(
|
||||
albumSortOption.label.t(context: context),
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: context.colorScheme.onSurface.withAlpha(225),
|
||||
),
|
||||
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(225)),
|
||||
),
|
||||
isSorting
|
||||
? SizedBox(
|
||||
@@ -542,7 +539,11 @@ class _QuickSortAndViewMode extends StatelessWidget {
|
||||
initialIsReverse: currentIsReverse,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24),
|
||||
icon: Icon(
|
||||
isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined,
|
||||
size: 24,
|
||||
color: context.colorScheme.onSurface,
|
||||
),
|
||||
onPressed: onToggleViewMode,
|
||||
),
|
||||
],
|
||||
@@ -662,6 +663,8 @@ class _GridAlbumCard extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? "");
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => onAlbumSelected(album),
|
||||
child: Card(
|
||||
@@ -680,12 +683,22 @@ class _GridAlbumCard extends ConsumerWidget {
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(15)),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: album.thumbnailAssetId != null
|
||||
? Thumbnail.remote(remoteId: album.thumbnailAssetId!)
|
||||
: Container(
|
||||
color: context.colorScheme.surfaceContainerHighest,
|
||||
child: const Icon(Icons.photo_album_rounded, size: 40, color: Colors.grey),
|
||||
),
|
||||
child: FutureBuilder(
|
||||
future: albumThumbnailAsset,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data != null) {
|
||||
return Thumbnail.remote(
|
||||
remoteId: album.thumbnailAssetId!,
|
||||
thumbhash: snapshot.data!.thumbHash ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
color: context.colorScheme.surfaceContainerHighest,
|
||||
child: const Icon(Icons.photo_album_rounded, size: 40, color: Colors.grey),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
|
||||
class AlbumTile extends StatelessWidget {
|
||||
class AlbumTile extends ConsumerWidget {
|
||||
const AlbumTile({super.key, required this.album, required this.isOwner, this.onAlbumSelected});
|
||||
|
||||
final RemoteAlbum album;
|
||||
@@ -14,7 +16,9 @@ class AlbumTile extends StatelessWidget {
|
||||
final Function(RemoteAlbum)? onAlbumSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? "");
|
||||
|
||||
return LargeLeadingTile(
|
||||
title: Text(
|
||||
album.name,
|
||||
@@ -29,23 +33,35 @@ class AlbumTile extends StatelessWidget {
|
||||
),
|
||||
onTap: () => onAlbumSelected?.call(album),
|
||||
leadingPadding: const EdgeInsets.only(right: 16),
|
||||
leading: album.thumbnailAssetId != null
|
||||
? ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
child: SizedBox(width: 80, height: 80, child: Thumbnail.remote(remoteId: album.thumbnailAssetId!)),
|
||||
)
|
||||
: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1),
|
||||
),
|
||||
child: const Icon(Icons.photo_album_rounded, size: 24, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
leading: FutureBuilder(
|
||||
future: albumThumbnailAsset,
|
||||
builder: (context, snapshot) {
|
||||
return snapshot.hasData && snapshot.data != null
|
||||
? ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: Thumbnail.remote(
|
||||
remoteId: album.thumbnailAssetId!,
|
||||
thumbhash: snapshot.data!.thumbHash ?? "",
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1),
|
||||
),
|
||||
child: const Icon(Icons.photo_album_rounded, size: 24, color: Colors.grey),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
@@ -164,11 +165,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
children: [
|
||||
if (albums.isNotEmpty)
|
||||
SheetTile(
|
||||
title: 'appears_in'.t(context: context).toUpperCase(),
|
||||
titleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
title: 'appears_in'.t(context: context),
|
||||
titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 24),
|
||||
@@ -224,9 +222,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
color: context.textTheme.labelLarge?.color,
|
||||
),
|
||||
subtitle: _getFileInfo(asset, exifInfo),
|
||||
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
),
|
||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -241,9 +237,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
color: context.textTheme.labelLarge?.color,
|
||||
),
|
||||
subtitle: _getFileInfo(asset, exifInfo),
|
||||
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
),
|
||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -262,11 +256,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
const SheetLocationDetails(),
|
||||
// Details header
|
||||
SheetTile(
|
||||
title: 'details'.t(context: context).toUpperCase(),
|
||||
titleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
title: 'details'.t(context: context),
|
||||
titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
// File info
|
||||
buildFileInfoTile(),
|
||||
@@ -278,9 +269,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||
subtitle: _getCameraInfoSubtitle(exifInfo),
|
||||
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
),
|
||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
],
|
||||
// Lens info
|
||||
@@ -291,15 +280,13 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||
subtitle: _getLensInfoSubtitle(exifInfo),
|
||||
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
),
|
||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
],
|
||||
// Appears in (Albums)
|
||||
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
||||
// padding at the bottom to avoid cut-off
|
||||
const SizedBox(height: 30),
|
||||
const SizedBox(height: 60),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
@@ -77,11 +78,8 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SheetTile(
|
||||
title: 'location'.t(context: context).toUpperCase(),
|
||||
titleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
title: 'location'.t(context: context),
|
||||
titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
trailing: hasCoordinates ? const Icon(Icons.edit_location_alt, size: 20) : null,
|
||||
onTap: editLocation,
|
||||
),
|
||||
@@ -105,9 +103,7 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
||||
),
|
||||
Text(
|
||||
coordinates,
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
),
|
||||
style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/people/person_edit_name_modal.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
@@ -53,11 +54,8 @@ class _SheetPeopleDetailsState extends ConsumerState<SheetPeopleDetails> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 16),
|
||||
child: Text(
|
||||
"people".t(context: context).toUpperCase(),
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
"people".t(context: context),
|
||||
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
|
||||
@@ -112,14 +112,17 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080
|
||||
provider = LocalFullImageProvider(id: id, size: size, assetType: asset.type);
|
||||
} else {
|
||||
final String assetId;
|
||||
final String thumbhash;
|
||||
if (asset is LocalAsset && asset.hasRemote) {
|
||||
assetId = asset.remoteId!;
|
||||
thumbhash = "";
|
||||
} else if (asset is RemoteAsset) {
|
||||
assetId = asset.id;
|
||||
thumbhash = asset.thumbHash ?? "";
|
||||
} else {
|
||||
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
|
||||
}
|
||||
provider = RemoteFullImageProvider(assetId: assetId);
|
||||
provider = RemoteFullImageProvider(assetId: assetId, thumbhash: thumbhash);
|
||||
}
|
||||
|
||||
return provider;
|
||||
@@ -132,8 +135,9 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai
|
||||
}
|
||||
|
||||
final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId;
|
||||
return assetId != null ? RemoteThumbProvider(assetId: assetId) : null;
|
||||
final thumbhash = asset is RemoteAsset ? asset.thumbHash ?? "" : "";
|
||||
return assetId != null ? RemoteThumbProvider(assetId: assetId, thumbhash: thumbhash) : null;
|
||||
}
|
||||
|
||||
bool _shouldUseLocalAsset(BaseAsset asset) =>
|
||||
asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage));
|
||||
asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)) && !asset.isEdited;
|
||||
|
||||
@@ -16,8 +16,9 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
|
||||
with CancellableImageProviderMixin<RemoteThumbProvider> {
|
||||
static final cacheManager = RemoteThumbnailCacheManager();
|
||||
final String assetId;
|
||||
final String thumbhash;
|
||||
|
||||
RemoteThumbProvider({required this.assetId});
|
||||
RemoteThumbProvider({required this.assetId, required this.thumbhash});
|
||||
|
||||
@override
|
||||
Future<RemoteThumbProvider> obtainKey(ImageConfiguration configuration) {
|
||||
@@ -38,7 +39,7 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
|
||||
|
||||
Stream<ImageInfo> _codec(RemoteThumbProvider key, ImageDecoderCallback decode) {
|
||||
final request = this.request = RemoteImageRequest(
|
||||
uri: getThumbnailUrlForRemoteId(key.assetId),
|
||||
uri: getThumbnailUrlForRemoteId(key.assetId, thumbhash: key.thumbhash),
|
||||
headers: ApiService.getRequestHeaders(),
|
||||
cacheManager: cacheManager,
|
||||
);
|
||||
@@ -49,22 +50,23 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is RemoteThumbProvider) {
|
||||
return assetId == other.assetId;
|
||||
return assetId == other.assetId && thumbhash == other.thumbhash;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => assetId.hashCode;
|
||||
int get hashCode => assetId.hashCode ^ thumbhash.hashCode;
|
||||
}
|
||||
|
||||
class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImageProvider>
|
||||
with CancellableImageProviderMixin<RemoteFullImageProvider> {
|
||||
static final cacheManager = RemoteThumbnailCacheManager();
|
||||
final String assetId;
|
||||
final String thumbhash;
|
||||
|
||||
RemoteFullImageProvider({required this.assetId});
|
||||
RemoteFullImageProvider({required this.assetId, required this.thumbhash});
|
||||
|
||||
@override
|
||||
Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
@@ -75,7 +77,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
|
||||
return OneFramePlaceholderImageStreamCompleter(
|
||||
_codec(key, decode),
|
||||
initialImage: getInitialImage(RemoteThumbProvider(assetId: key.assetId)),
|
||||
initialImage: getInitialImage(RemoteThumbProvider(assetId: key.assetId, thumbhash: key.thumbhash)),
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
||||
@@ -94,7 +96,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
|
||||
final headers = ApiService.getRequestHeaders();
|
||||
final request = this.request = RemoteImageRequest(
|
||||
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview),
|
||||
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash),
|
||||
headers: headers,
|
||||
cacheManager: cacheManager,
|
||||
);
|
||||
@@ -115,12 +117,12 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is RemoteFullImageProvider) {
|
||||
return assetId == other.assetId;
|
||||
return assetId == other.assetId && thumbhash == other.thumbhash;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => assetId.hashCode;
|
||||
int get hashCode => assetId.hashCode ^ thumbhash.hashCode;
|
||||
}
|
||||
|
||||
@@ -21,9 +21,14 @@ class Thumbnail extends StatefulWidget {
|
||||
|
||||
const Thumbnail({this.imageProvider, this.fit = BoxFit.cover, this.thumbhashProvider, super.key});
|
||||
|
||||
Thumbnail.remote({required String remoteId, this.fit = BoxFit.cover, Size size = kThumbnailResolution, super.key})
|
||||
: imageProvider = RemoteThumbProvider(assetId: remoteId),
|
||||
thumbhashProvider = null;
|
||||
Thumbnail.remote({
|
||||
required String remoteId,
|
||||
required String thumbhash,
|
||||
this.fit = BoxFit.cover,
|
||||
Size size = kThumbnailResolution,
|
||||
super.key,
|
||||
}) : imageProvider = RemoteThumbProvider(assetId: remoteId, thumbhash: thumbhash),
|
||||
thumbhashProvider = null;
|
||||
|
||||
Thumbnail.fromAsset({
|
||||
required BaseAsset? asset,
|
||||
|
||||
@@ -60,7 +60,11 @@ class DriftMemoryCard extends ConsumerWidget {
|
||||
child: SizedBox(
|
||||
width: 205,
|
||||
height: 200,
|
||||
child: Thumbnail.remote(remoteId: memory.assets[0].id, fit: BoxFit.cover),
|
||||
child: Thumbnail.remote(
|
||||
remoteId: memory.assets[0].id,
|
||||
thumbhash: memory.assets[0].thumbHash ?? "",
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
|
||||
@@ -160,7 +160,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
_resumeBackup();
|
||||
}),
|
||||
_resumeBackup(),
|
||||
backgroundManager.syncCloudIds(),
|
||||
_safeRun(backgroundManager.syncCloudIds(), "syncCloudIds"),
|
||||
]);
|
||||
} else {
|
||||
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
|
||||
|
||||
@@ -69,6 +69,7 @@ class CastNotifier extends StateNotifier<CastManagerState> {
|
||||
: AssetType.other,
|
||||
createdAt: asset.fileCreatedAt,
|
||||
updatedAt: asset.updatedAt,
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
_gCastService.loadMedia(remoteAsset, reload);
|
||||
|
||||
@@ -144,6 +144,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
socket.on('on_asset_hidden', _handleOnAssetHidden);
|
||||
} else {
|
||||
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
|
||||
socket.on('AssetEditReadyV1', _handleSyncAssetEditReady);
|
||||
}
|
||||
|
||||
socket.on('on_config_update', _handleOnConfigUpdate);
|
||||
@@ -192,10 +193,12 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
|
||||
void stopListeningToBetaEvents() {
|
||||
state.socket?.off('AssetUploadReadyV1');
|
||||
state.socket?.off('AssetEditReadyV1');
|
||||
}
|
||||
|
||||
void startListeningToBetaEvents() {
|
||||
state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
|
||||
state.socket?.on('AssetEditReadyV1', _handleSyncAssetEditReady);
|
||||
}
|
||||
|
||||
void listenUploadEvent() {
|
||||
@@ -315,6 +318,10 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
_batchDebouncer.run(_processBatchedAssetUploadReady);
|
||||
}
|
||||
|
||||
void _handleSyncAssetEditReady(dynamic data) {
|
||||
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditBatch([data]));
|
||||
}
|
||||
|
||||
void _processBatchedAssetUploadReady() {
|
||||
if (_batchedAssetUploadReady.isEmpty) {
|
||||
return;
|
||||
|
||||
@@ -25,6 +25,7 @@ class FileMediaRepository {
|
||||
type: AssetType.image,
|
||||
createdAt: entity.createDateTime,
|
||||
updatedAt: entity.modifiedDateTime,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,12 @@ ThemeData getThemeData({required ColorScheme colorScheme, required Locale locale
|
||||
),
|
||||
),
|
||||
chipTheme: const ChipThemeData(side: BorderSide.none),
|
||||
sliderTheme: const SliderThemeData(thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), trackHeight: 2.0),
|
||||
sliderTheme: const SliderThemeData(
|
||||
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7),
|
||||
trackHeight: 2.0,
|
||||
// ignore: deprecated_member_use
|
||||
year2023: false,
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed),
|
||||
popupMenuTheme: const PopupMenuThemeData(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
|
||||
@@ -50,8 +50,10 @@ String getThumbnailUrlForRemoteId(
|
||||
final String id, {
|
||||
AssetMediaSize type = AssetMediaSize.thumbnail,
|
||||
bool edited = true,
|
||||
String? thumbhash,
|
||||
}) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}&edited=$edited';
|
||||
final url = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}&edited=$edited';
|
||||
return thumbhash != null ? '$url&c=${Uri.encodeComponent(thumbhash)}' : url;
|
||||
}
|
||||
|
||||
String getPlaybackUrlForRemoteId(final String id) {
|
||||
|
||||
@@ -49,7 +49,7 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||
}
|
||||
case 'SyncAssetV1':
|
||||
if (value is Map) {
|
||||
addDefault(value, 'editCount', 0);
|
||||
addDefault(value, 'isEdited', false);
|
||||
}
|
||||
case 'ServerFeaturesDto':
|
||||
if (value is Map) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
||||
|
||||
class GroupSettings extends HookConsumerWidget {
|
||||
const GroupSettings({super.key});
|
||||
@@ -33,12 +33,24 @@ class GroupSettings extends HookConsumerWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingsSubTitle(title: "asset_list_group_by_sub_title".tr()),
|
||||
SettingGroupTitle(
|
||||
title: "asset_list_group_by_sub_title".t(context: context),
|
||||
icon: Icons.group_work_outlined,
|
||||
),
|
||||
SettingsRadioListTile(
|
||||
groups: [
|
||||
SettingsRadioGroup(title: 'asset_list_layout_settings_group_by_month_day'.tr(), value: GroupAssetsBy.day),
|
||||
SettingsRadioGroup(title: 'month'.tr(), value: GroupAssetsBy.month),
|
||||
SettingsRadioGroup(title: 'asset_list_layout_settings_group_automatically'.tr(), value: GroupAssetsBy.auto),
|
||||
SettingsRadioGroup(
|
||||
title: 'asset_list_layout_settings_group_by_month_day'.t(context: context),
|
||||
value: GroupAssetsBy.day,
|
||||
),
|
||||
SettingsRadioGroup(
|
||||
title: 'month'.t(context: context),
|
||||
value: GroupAssetsBy.month,
|
||||
),
|
||||
SettingsRadioGroup(
|
||||
title: 'asset_list_layout_settings_group_automatically'.t(context: context),
|
||||
value: GroupAssetsBy.auto,
|
||||
),
|
||||
],
|
||||
groupBy: groupBy,
|
||||
onRadioChanged: changeGroupValue,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
|
||||
class LayoutSettings extends HookConsumerWidget {
|
||||
@@ -19,10 +20,13 @@ class LayoutSettings extends HookConsumerWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingsSubTitle(title: "asset_list_layout_sub_title".tr()),
|
||||
SettingGroupTitle(
|
||||
title: "asset_list_layout_sub_title".t(context: context),
|
||||
icon: Icons.view_module_outlined,
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: useDynamicLayout,
|
||||
title: "asset_list_layout_settings_dynamic_layout_title".tr(),
|
||||
title: "asset_list_layout_settings_dynamic_layout_title".t(context: context),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
SettingsSliderListTile(
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
|
||||
@@ -19,21 +18,21 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingsSubTitle(title: "setting_image_viewer_title".tr()),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
title: Text('setting_image_viewer_help', style: context.textTheme.bodyMedium).tr(),
|
||||
SettingGroupTitle(
|
||||
title: "photos".t(context: context),
|
||||
icon: Icons.image_outlined,
|
||||
subtitle: "setting_image_viewer_help".t(context: context),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isPreview,
|
||||
title: "setting_image_viewer_preview_title".tr(),
|
||||
subtitle: "setting_image_viewer_preview_subtitle".tr(),
|
||||
title: "setting_image_viewer_preview_title".t(context: context),
|
||||
subtitle: "setting_image_viewer_preview_subtitle".t(context: context),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isOriginal,
|
||||
title: "setting_image_viewer_original_title".tr(),
|
||||
subtitle: "setting_image_viewer_original_subtitle".tr(),
|
||||
title: "setting_image_viewer_original_title".t(context: context),
|
||||
subtitle: "setting_image_viewer_original_subtitle".t(context: context),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
|
||||
@@ -19,23 +19,26 @@ class VideoViewerSettings extends HookConsumerWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingsSubTitle(title: "videos".tr()),
|
||||
SettingGroupTitle(
|
||||
title: "videos".t(context: context),
|
||||
icon: Icons.video_camera_back_outlined,
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: useAutoPlayVideo,
|
||||
title: "setting_video_viewer_auto_play_title".tr(),
|
||||
subtitle: "setting_video_viewer_auto_play_subtitle".tr(),
|
||||
title: "setting_video_viewer_auto_play_title".t(context: context),
|
||||
subtitle: "setting_video_viewer_auto_play_subtitle".t(context: context),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: useLoopVideo,
|
||||
title: "setting_video_viewer_looping_title".tr(),
|
||||
subtitle: "loop_videos_description".tr(),
|
||||
title: "setting_video_viewer_looping_title".t(context: context),
|
||||
subtitle: "loop_videos_description".t(context: context),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: useOriginalVideo,
|
||||
title: "setting_video_viewer_original_video_title".tr(),
|
||||
subtitle: "setting_video_viewer_original_video_subtitle".tr(),
|
||||
title: "setting_video_viewer_original_video_title".t(context: context),
|
||||
subtitle: "setting_video_viewer_original_video_subtitle".t(context: context),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -16,6 +16,8 @@ import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_list_tile.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||
|
||||
class DriftBackupSettings extends ConsumerWidget {
|
||||
@@ -25,36 +27,25 @@ class DriftBackupSettings extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return SettingsSubPageScaffold(
|
||||
settings: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
"network_requirements".t(context: context).toUpperCase(),
|
||||
style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)),
|
||||
),
|
||||
SettingGroupTitle(
|
||||
title: "network_requirements".t(context: context),
|
||||
icon: Icons.cell_tower,
|
||||
),
|
||||
const _UseWifiForUploadVideosButton(),
|
||||
const _UseWifiForUploadPhotosButton(),
|
||||
if (CurrentPlatform.isAndroid) ...[
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
"background_options".t(context: context).toUpperCase(),
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
SettingGroupTitle(
|
||||
title: "background_options".t(context: context),
|
||||
icon: Icons.charging_station_rounded,
|
||||
),
|
||||
const _BackupOnlyWhenChargingButton(),
|
||||
const _BackupDelaySlider(),
|
||||
],
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
"backup_albums_sync".t(context: context).toUpperCase(),
|
||||
style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)),
|
||||
),
|
||||
SettingGroupTitle(
|
||||
title: "backup_albums_sync".t(context: context),
|
||||
icon: Icons.sync,
|
||||
),
|
||||
const _AlbumSyncActionButton(),
|
||||
],
|
||||
@@ -105,81 +96,67 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
StreamBuilder(
|
||||
stream: Store.watch(StoreKey.syncAlbums),
|
||||
initialData: Store.tryGet(StoreKey.syncAlbums) ?? false,
|
||||
builder: (context, snapshot) {
|
||||
final albumSyncEnable = snapshot.data ?? false;
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
"sync_albums".t(context: context),
|
||||
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
|
||||
),
|
||||
subtitle: Text(
|
||||
"sync_upload_album_setting_subtitle".t(context: context),
|
||||
style: context.textTheme.labelLarge,
|
||||
),
|
||||
trailing: Switch(
|
||||
value: albumSyncEnable,
|
||||
onChanged: (bool newValue) async {
|
||||
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
StreamBuilder(
|
||||
stream: Store.watch(StoreKey.syncAlbums),
|
||||
initialData: Store.tryGet(StoreKey.syncAlbums) ?? false,
|
||||
builder: (context, snapshot) {
|
||||
final albumSyncEnable = snapshot.data ?? false;
|
||||
return Column(
|
||||
children: [
|
||||
SettingListTile(
|
||||
title: "sync_albums".t(context: context),
|
||||
subtitle: "sync_upload_album_setting_subtitle".t(context: context),
|
||||
trailing: Switch(
|
||||
value: albumSyncEnable,
|
||||
onChanged: (bool newValue) async {
|
||||
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue);
|
||||
|
||||
if (newValue == true) {
|
||||
await _manageLinkedAlbums();
|
||||
}
|
||||
},
|
||||
if (newValue == true) {
|
||||
await _manageLinkedAlbums();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: albumSyncEnable ? 1.0 : 0.0,
|
||||
child: albumSyncEnable
|
||||
? ListTile(
|
||||
onTap: _manualSyncAlbums,
|
||||
contentPadding: const EdgeInsets.only(left: 32, right: 16),
|
||||
title: Text(
|
||||
"organize_into_albums".t(context: context),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
"organize_into_albums_description".t(context: context),
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
trailing: isAlbumSyncInProgress
|
||||
? const SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: _manualSyncAlbums,
|
||||
icon: const Icon(Icons.sync_rounded),
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
iconSize: 20,
|
||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: albumSyncEnable ? 1.0 : 0.0,
|
||||
child: albumSyncEnable
|
||||
? SettingListTile(
|
||||
onTap: _manualSyncAlbums,
|
||||
contentPadding: const EdgeInsets.only(left: 32, right: 16),
|
||||
title: "organize_into_albums".t(context: context),
|
||||
subtitle: "organize_into_albums_description".t(context: context),
|
||||
trailing: isAlbumSyncInProgress
|
||||
? const SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: _manualSyncAlbums,
|
||||
icon: const Icon(Icons.sync_rounded),
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
iconSize: 20,
|
||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -222,24 +199,24 @@ class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
widget.titleKey.t(context: context),
|
||||
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
|
||||
),
|
||||
subtitle: Text(widget.subtitleKey.t(context: context), style: context.textTheme.labelLarge),
|
||||
trailing: StreamBuilder(
|
||||
stream: valueStream,
|
||||
initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue,
|
||||
builder: (context, snapshot) {
|
||||
final value = snapshot.data ?? false;
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: (bool newValue) async {
|
||||
await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue);
|
||||
},
|
||||
);
|
||||
},
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: SettingListTile(
|
||||
title: widget.titleKey.t(context: context),
|
||||
subtitle: widget.subtitleKey.t(context: context),
|
||||
trailing: StreamBuilder(
|
||||
stream: valueStream,
|
||||
initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue,
|
||||
builder: (context, snapshot) {
|
||||
final value = snapshot.data ?? false;
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: (bool newValue) async {
|
||||
await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -354,7 +331,7 @@ class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> {
|
||||
'backup_controller_page_background_delay'.tr(
|
||||
namedArgs: {'duration': formatBackupDelaySliderValue(currentValue)},
|
||||
),
|
||||
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
|
||||
style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
|
||||
@@ -34,33 +34,36 @@ class EntityCountTile extends StatelessWidget {
|
||||
children: [
|
||||
// Icon and Label
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: context.primaryColor),
|
||||
const SizedBox(width: 8),
|
||||
Icon(icon, color: context.primaryColor, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Number
|
||||
const Spacer(),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontSize: 18, fontFamily: 'GoogleSansCode', fontWeight: FontWeight.w600),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: zeroPadding(count, maxDigits),
|
||||
style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
|
||||
),
|
||||
TextSpan(
|
||||
text: count.toString(),
|
||||
style: TextStyle(color: context.primaryColor),
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontSize: 18, fontFamily: 'GoogleSansCode'),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: zeroPadding(count, maxDigits),
|
||||
style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
|
||||
),
|
||||
TextSpan(
|
||||
text: count.toString(),
|
||||
style: TextStyle(color: context.colorScheme.onSurface),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -16,6 +16,8 @@ import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart'
|
||||
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_list_tile.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
@@ -112,48 +114,39 @@ class SyncStatusAndActions extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 96),
|
||||
children: [
|
||||
const _SyncStatsCounts(),
|
||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
const SizedBox(height: 24),
|
||||
_SectionHeaderText(text: "jobs".t(context: context)),
|
||||
ListTile(
|
||||
title: Text(
|
||||
"sync_local".t(context: context),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: Text("tap_to_run_job".t(context: context)),
|
||||
const Divider(height: 10),
|
||||
const SizedBox(height: 16),
|
||||
SettingGroupTitle(title: "jobs".t(context: context)),
|
||||
SettingListTile(
|
||||
title: "sync_local".t(context: context),
|
||||
subtitle: "tap_to_run_job".t(context: context),
|
||||
leading: const Icon(Icons.sync),
|
||||
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).localSyncStatus),
|
||||
onTap: () {
|
||||
ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
"sync_remote".t(context: context),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: Text("tap_to_run_job".t(context: context)),
|
||||
SettingListTile(
|
||||
title: "sync_remote".t(context: context),
|
||||
subtitle: "tap_to_run_job".t(context: context),
|
||||
leading: const Icon(Icons.cloud_sync),
|
||||
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).remoteSyncStatus),
|
||||
onTap: () {
|
||||
ref.read(backgroundSyncProvider).syncRemote();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
"hash_asset".t(context: context),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
SettingListTile(
|
||||
title: "hash_asset".t(context: context),
|
||||
leading: const Icon(Icons.tag),
|
||||
subtitle: Text("tap_to_run_job".t(context: context)),
|
||||
subtitle: "tap_to_run_job".t(context: context),
|
||||
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).hashJobStatus),
|
||||
onTap: () {
|
||||
ref.read(backgroundSyncProvider).hashAssets();
|
||||
},
|
||||
),
|
||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
const SizedBox(height: 24),
|
||||
_SectionHeaderText(text: "actions".t(context: context)),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 16),
|
||||
SettingGroupTitle(title: "actions".t(context: context)),
|
||||
ListTile(
|
||||
title: Text(
|
||||
"clear_file_cache".t(context: context),
|
||||
@@ -202,26 +195,6 @@ class _SyncStatusIcon extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeaderText extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
const _SectionHeaderText({required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
text.toUpperCase(),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: context.colorScheme.onSurface.withAlpha(200),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SyncStatsCounts extends ConsumerWidget {
|
||||
const _SyncStatsCounts();
|
||||
|
||||
@@ -279,9 +252,9 @@ class _SyncStatsCounts extends ConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_SectionHeaderText(text: "assets".t(context: context)),
|
||||
SettingGroupTitle(title: "assets".t(context: context)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
// 1. Wrap in IntrinsicHeight
|
||||
child: IntrinsicHeight(
|
||||
child: Flex(
|
||||
@@ -309,9 +282,9 @@ class _SyncStatsCounts extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
_SectionHeaderText(text: "albums".t(context: context)),
|
||||
SettingGroupTitle(title: "albums".t(context: context)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: IntrinsicHeight(
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
@@ -337,9 +310,9 @@ class _SyncStatsCounts extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
_SectionHeaderText(text: "other".t(context: context)),
|
||||
SettingGroupTitle(title: "other".t(context: context)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: IntrinsicHeight(
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
@@ -368,7 +341,7 @@ class _SyncStatsCounts extends ConsumerWidget {
|
||||
// To be removed once the experimental feature is stable
|
||||
if (CurrentPlatform.isAndroid &&
|
||||
appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) ...[
|
||||
_SectionHeaderText(text: "trash".t(context: context)),
|
||||
SettingGroupTitle(title: "trash".t(context: context)),
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final counts = ref.watch(trashedAssetsCountProvider);
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_list_tile.dart';
|
||||
|
||||
class BetaTimelineListTile extends ConsumerWidget {
|
||||
const BetaTimelineListTile({super.key});
|
||||
@@ -56,8 +57,8 @@ class BetaTimelineListTile extends ConsumerWidget {
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: ListTile(
|
||||
title: Text("new_timeline".t(context: context)),
|
||||
child: SettingListTile(
|
||||
title: "new_timeline".t(context: context),
|
||||
trailing: Switch.adaptive(
|
||||
value: betaTimelineValue,
|
||||
onChanged: onSwitchChanged,
|
||||
|
||||
@@ -142,7 +142,9 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
||||
final state = ref.watch(cleanupProvider);
|
||||
final hasDate = state.selectedDate != null;
|
||||
final hasAssets = _hasScanned && state.assetsToDelete.isNotEmpty;
|
||||
|
||||
final subtitleStyle = context.textTheme.bodyMedium!.copyWith(
|
||||
color: context.textTheme.bodyMedium!.color!.withAlpha(215),
|
||||
);
|
||||
StepStyle styleForState(StepState stepState, {bool isDestructive = false}) {
|
||||
switch (stepState) {
|
||||
case StepState.complete:
|
||||
@@ -214,10 +216,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
border: Border.all(color: context.primaryColor.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Text(
|
||||
'free_up_space_description'.t(context: context),
|
||||
style: context.textTheme.labelLarge?.copyWith(fontSize: 15),
|
||||
),
|
||||
child: Text('free_up_space_description'.t(context: context), style: context.textTheme.bodyMedium),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -256,7 +255,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('cutoff_date_description'.t(context: context), style: context.textTheme.labelLarge),
|
||||
Text('cutoff_date_description'.t(context: context), style: subtitleStyle),
|
||||
const SizedBox(height: 16),
|
||||
GridView.count(
|
||||
shrinkWrap: true,
|
||||
@@ -352,7 +351,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('cleanup_filter_description'.t(context: context), style: context.textTheme.labelLarge),
|
||||
Text('cleanup_filter_description'.t(context: context), style: subtitleStyle),
|
||||
const SizedBox(height: 16),
|
||||
SegmentedButton<AssetFilterType>(
|
||||
segments: [
|
||||
@@ -381,10 +380,15 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text('keep_favorites'.t(context: context), style: context.textTheme.titleSmall),
|
||||
title: Text(
|
||||
'keep_favorites'.t(context: context),
|
||||
style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5),
|
||||
),
|
||||
subtitle: Text(
|
||||
'keep_favorites_description'.t(context: context),
|
||||
style: context.textTheme.labelLarge,
|
||||
style: context.textTheme.bodyMedium!.copyWith(
|
||||
color: context.textTheme.bodyMedium!.color!.withAlpha(215),
|
||||
),
|
||||
),
|
||||
value: state.keepFavorites,
|
||||
onChanged: (value) {
|
||||
@@ -435,10 +439,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
||||
: null,
|
||||
content: Column(
|
||||
children: [
|
||||
Text(
|
||||
'cleanup_step3_description'.t(context: context),
|
||||
style: context.textTheme.labelLarge?.copyWith(fontSize: 15),
|
||||
),
|
||||
Text('cleanup_step3_description'.t(context: context), style: subtitleStyle),
|
||||
if (CurrentPlatform.isIOS) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
|
||||
@@ -117,7 +117,7 @@ class EndpointInputState extends ConsumerState<EndpointInput> {
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: validateUrl,
|
||||
keyboardType: TextInputType.url,
|
||||
style: const TextStyle(fontFamily: 'GoogleSansCode', fontWeight: FontWeight.w600, fontSize: 14),
|
||||
style: const TextStyle(fontFamily: 'GoogleSansCode', fontSize: 14),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'http(s)://immich.domain.com',
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||
import 'package:immich_mobile/widgets/settings/networking_settings/endpoint_input.dart';
|
||||
|
||||
@@ -103,7 +103,7 @@ class ExternalNetworkPreference extends HookConsumerWidget {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 24),
|
||||
child: Text("external_network_sheet_info".tr(), style: context.textTheme.bodyMedium),
|
||||
child: Text("external_network_sheet_info".t(context: context), style: context.textTheme.bodyMedium),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Divider(color: context.colorScheme.surfaceContainerHighest),
|
||||
@@ -135,7 +135,7 @@ class ExternalNetworkPreference extends HookConsumerWidget {
|
||||
height: 48,
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text('add_endpoint'.tr().toUpperCase()),
|
||||
label: Text('add_endpoint'.t(context: context)),
|
||||
onPressed: enabled
|
||||
? () {
|
||||
entries.value = [
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/network.provider.dart';
|
||||
|
||||
@@ -167,13 +168,12 @@ class LocalNetworkPreference extends HookConsumerWidget {
|
||||
enabled: enabled,
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 8),
|
||||
leading: const Icon(Icons.lan_rounded),
|
||||
title: Text("server_endpoint".tr()),
|
||||
title: Text("server_endpoint".t(context: context)),
|
||||
subtitle: localEndpointText.value.isEmpty
|
||||
? const Text("http://local-ip:2283")
|
||||
: Text(
|
||||
localEndpointText.value,
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100),
|
||||
fontFamily: 'GoogleSansCode',
|
||||
),
|
||||
@@ -190,7 +190,7 @@ class LocalNetworkPreference extends HookConsumerWidget {
|
||||
height: 48,
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.wifi_find_rounded),
|
||||
label: Text('use_current_connection'.tr().toUpperCase()),
|
||||
label: Text('use_current_connection'.t(context: context)),
|
||||
onPressed: enabled ? autofillCurrentNetwork : null,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||
import 'package:immich_mobile/providers/network.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
@@ -10,6 +11,7 @@ import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
import 'package:immich_mobile/widgets/settings/networking_settings/external_network_preference.dart';
|
||||
import 'package:immich_mobile/widgets/settings/networking_settings/local_network_preference.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
|
||||
class NetworkingSettings extends HookConsumerWidget {
|
||||
@@ -87,12 +89,10 @@ class NetworkingSettings extends HookConsumerWidget {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.only(bottom: 96),
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8, left: 16, bottom: 8),
|
||||
child: NetworkPreferenceTitle(
|
||||
title: "current_server_address".tr().toUpperCase(),
|
||||
icon: (currentEndpoint?.startsWith('https') ?? false) ? Icons.https_outlined : Icons.http_outlined,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SettingGroupTitle(
|
||||
title: "current_server_address".t(context: context),
|
||||
icon: (currentEndpoint?.startsWith('https') ?? false) ? Icons.https_outlined : Icons.http_outlined,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
@@ -108,12 +108,7 @@ class NetworkingSettings extends HookConsumerWidget {
|
||||
: const Icon(Icons.circle_outlined),
|
||||
title: Text(
|
||||
currentEndpoint ?? "--",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'GoogleSansCode',
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
style: TextStyle(fontSize: 14, fontFamily: 'GoogleSansCode', color: context.primaryColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -128,14 +123,16 @@ class NetworkingSettings extends HookConsumerWidget {
|
||||
title: "automatic_endpoint_switching_title".tr(),
|
||||
subtitle: "automatic_endpoint_switching_subtitle".tr(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8, left: 16, bottom: 16),
|
||||
child: NetworkPreferenceTitle(title: "local_network".tr().toUpperCase(), icon: Icons.home_outlined),
|
||||
const SizedBox(height: 8),
|
||||
SettingGroupTitle(
|
||||
title: "local_network".t(context: context),
|
||||
icon: Icons.home_outlined,
|
||||
),
|
||||
LocalNetworkPreference(enabled: featureEnabled.value),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 32, left: 16, bottom: 16),
|
||||
child: NetworkPreferenceTitle(title: "external_network".tr().toUpperCase(), icon: Icons.dns_outlined),
|
||||
const SizedBox(height: 16),
|
||||
SettingGroupTitle(
|
||||
title: "external_network".t(context: context),
|
||||
icon: Icons.dns_outlined,
|
||||
),
|
||||
ExternalNetworkPreference(enabled: featureEnabled.value),
|
||||
],
|
||||
@@ -143,30 +140,6 @@ class NetworkingSettings extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkPreferenceTitle extends StatelessWidget {
|
||||
const NetworkPreferenceTitle({super.key, required this.icon, required this.title});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, color: context.colorScheme.onSurface.withAlpha(150)),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: context.textTheme.displaySmall?.copyWith(
|
||||
color: context.colorScheme.onSurface.withAlpha(200),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkStatusIcon extends StatelessWidget {
|
||||
const NetworkStatusIcon({super.key, required this.status, this.enabled = true}) : super();
|
||||
|
||||
@@ -175,10 +148,10 @@ class NetworkStatusIcon extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSwitcher(duration: const Duration(milliseconds: 200), child: _buildIcon(context));
|
||||
return AnimatedSwitcher(duration: const Duration(milliseconds: 200), child: buildIcon(context));
|
||||
}
|
||||
|
||||
Widget _buildIcon(BuildContext context) => switch (status) {
|
||||
Widget buildIcon(BuildContext context) => switch (status) {
|
||||
AuxCheckStatus.loading => Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: SizedBox(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
|
||||
@@ -22,10 +22,13 @@ class HapticSetting extends HookConsumerWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingsSubTitle(title: "haptic_feedback_title".tr()),
|
||||
SettingGroupTitle(
|
||||
title: "haptic_feedback_title".t(context: context),
|
||||
icon: Icons.vibration_outlined,
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isHapticFeedbackEnabled,
|
||||
title: 'haptic_feedback_switch'.tr(),
|
||||
title: 'enabled'.t(context: context),
|
||||
onChanged: onHapticFeedbackChange,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
|
||||
@@ -74,23 +74,26 @@ class ThemeSetting extends HookConsumerWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingsSubTitle(title: "theme".tr()),
|
||||
SettingGroupTitle(
|
||||
title: "theme".t(context: context),
|
||||
icon: Icons.color_lens_outlined,
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isSystemTheme,
|
||||
title: 'theme_setting_system_theme_switch'.tr(),
|
||||
title: 'theme_setting_system_theme_switch'.t(context: context),
|
||||
onChanged: onSystemThemeChange,
|
||||
),
|
||||
if (currentTheme.value != ThemeMode.system)
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isDarkTheme,
|
||||
title: 'map_settings_dark_mode'.tr(),
|
||||
title: 'map_settings_dark_mode'.t(context: context),
|
||||
onChanged: onThemeChange,
|
||||
),
|
||||
const PrimaryColorSetting(),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: applyThemeToBackgroundProvider,
|
||||
title: "theme_setting_colorful_interface_title".tr(),
|
||||
subtitle: 'theme_setting_colorful_interface_subtitle'.tr(),
|
||||
title: "theme_setting_colorful_interface_title".t(context: context),
|
||||
subtitle: 'theme_setting_colorful_interface_subtitle'.t(context: context),
|
||||
onChanged: onSurfaceColorSettingChange,
|
||||
),
|
||||
],
|
||||
|
||||
39
mobile/lib/widgets/settings/setting_group_title.dart
Normal file
39
mobile/lib/widgets/settings/setting_group_title.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
|
||||
class SettingGroupTitle extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final IconData? icon;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
|
||||
const SettingGroupTitle({super.key, required this.title, this.icon, this.subtitle, this.contentPadding});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: contentPadding ?? const EdgeInsets.only(left: 20.0, right: 20.0, bottom: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, color: context.colorScheme.onSurfaceSecondary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Text(title, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary)),
|
||||
],
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: context.textTheme.bodyMedium!.copyWith(color: context.colorScheme.onSurface.withAlpha(200)),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
38
mobile/lib/widgets/settings/setting_list_tile.dart
Normal file
38
mobile/lib/widgets/settings/setting_list_tile.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class SettingListTile extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
final VoidCallback? onTap;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
|
||||
const SettingListTile({
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
this.contentPadding,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(title, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5)),
|
||||
subtitle: subtitle != null
|
||||
? Text(
|
||||
subtitle!,
|
||||
style: context.textTheme.bodyMedium!.copyWith(color: context.textTheme.bodyMedium!.color!.withAlpha(215)),
|
||||
)
|
||||
: null,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
onTap: onTap,
|
||||
contentPadding: contentPadding,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -36,11 +36,8 @@ class SettingsCard extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Icon(icon, color: context.primaryColor),
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: context.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor),
|
||||
),
|
||||
subtitle: Text(subtitle, style: context.textTheme.labelLarge),
|
||||
title: Text(title, style: context.textTheme.titleMedium!.copyWith(color: context.primaryColor)),
|
||||
subtitle: Text(subtitle, style: context.textTheme.bodyMedium),
|
||||
onTap: () => context.pushRoute(settingRoute),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -9,13 +9,11 @@ class SettingsSubPageScaffold extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
itemCount: settings.length,
|
||||
itemBuilder: (ctx, index) => settings[index],
|
||||
separatorBuilder: (context, index) => showDivider
|
||||
? const Column(
|
||||
children: [SizedBox(height: 5), Divider(height: 10, indent: 15, endIndent: 15), SizedBox(height: 15)],
|
||||
)
|
||||
? const Column(children: [SizedBox(height: 5), Divider(height: 10), SizedBox(height: 15)])
|
||||
: const SizedBox(height: 10),
|
||||
);
|
||||
}
|
||||
|
||||
18
mobile/openapi/lib/model/sync_asset_v1.dart
generated
18
mobile/openapi/lib/model/sync_asset_v1.dart
generated
@@ -16,11 +16,11 @@ class SyncAssetV1 {
|
||||
required this.checksum,
|
||||
required this.deletedAt,
|
||||
required this.duration,
|
||||
required this.editCount,
|
||||
required this.fileCreatedAt,
|
||||
required this.fileModifiedAt,
|
||||
required this.height,
|
||||
required this.id,
|
||||
required this.isEdited,
|
||||
required this.isFavorite,
|
||||
required this.libraryId,
|
||||
required this.livePhotoVideoId,
|
||||
@@ -40,8 +40,6 @@ class SyncAssetV1 {
|
||||
|
||||
String? duration;
|
||||
|
||||
int editCount;
|
||||
|
||||
DateTime? fileCreatedAt;
|
||||
|
||||
DateTime? fileModifiedAt;
|
||||
@@ -50,6 +48,8 @@ class SyncAssetV1 {
|
||||
|
||||
String id;
|
||||
|
||||
bool isEdited;
|
||||
|
||||
bool isFavorite;
|
||||
|
||||
String? libraryId;
|
||||
@@ -77,11 +77,11 @@ class SyncAssetV1 {
|
||||
other.checksum == checksum &&
|
||||
other.deletedAt == deletedAt &&
|
||||
other.duration == duration &&
|
||||
other.editCount == editCount &&
|
||||
other.fileCreatedAt == fileCreatedAt &&
|
||||
other.fileModifiedAt == fileModifiedAt &&
|
||||
other.height == height &&
|
||||
other.id == id &&
|
||||
other.isEdited == isEdited &&
|
||||
other.isFavorite == isFavorite &&
|
||||
other.libraryId == libraryId &&
|
||||
other.livePhotoVideoId == livePhotoVideoId &&
|
||||
@@ -100,11 +100,11 @@ class SyncAssetV1 {
|
||||
(checksum.hashCode) +
|
||||
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
||||
(duration == null ? 0 : duration!.hashCode) +
|
||||
(editCount.hashCode) +
|
||||
(fileCreatedAt == null ? 0 : fileCreatedAt!.hashCode) +
|
||||
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
|
||||
(height == null ? 0 : height!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isEdited.hashCode) +
|
||||
(isFavorite.hashCode) +
|
||||
(libraryId == null ? 0 : libraryId!.hashCode) +
|
||||
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
||||
@@ -118,7 +118,7 @@ class SyncAssetV1 {
|
||||
(width == null ? 0 : width!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, editCount=$editCount, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, height=$height, id=$id, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility, width=$width]';
|
||||
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, height=$height, id=$id, isEdited=$isEdited, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility, width=$width]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -133,7 +133,6 @@ class SyncAssetV1 {
|
||||
} else {
|
||||
// json[r'duration'] = null;
|
||||
}
|
||||
json[r'editCount'] = this.editCount;
|
||||
if (this.fileCreatedAt != null) {
|
||||
json[r'fileCreatedAt'] = this.fileCreatedAt!.toUtc().toIso8601String();
|
||||
} else {
|
||||
@@ -150,6 +149,7 @@ class SyncAssetV1 {
|
||||
// json[r'height'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'isEdited'] = this.isEdited;
|
||||
json[r'isFavorite'] = this.isFavorite;
|
||||
if (this.libraryId != null) {
|
||||
json[r'libraryId'] = this.libraryId;
|
||||
@@ -200,11 +200,11 @@ class SyncAssetV1 {
|
||||
checksum: mapValueOfType<String>(json, r'checksum')!,
|
||||
deletedAt: mapDateTime(json, r'deletedAt', r''),
|
||||
duration: mapValueOfType<String>(json, r'duration'),
|
||||
editCount: mapValueOfType<int>(json, r'editCount')!,
|
||||
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r''),
|
||||
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
|
||||
height: mapValueOfType<int>(json, r'height'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isEdited: mapValueOfType<bool>(json, r'isEdited')!,
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||
libraryId: mapValueOfType<String>(json, r'libraryId'),
|
||||
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||
@@ -266,11 +266,11 @@ class SyncAssetV1 {
|
||||
'checksum',
|
||||
'deletedAt',
|
||||
'duration',
|
||||
'editCount',
|
||||
'fileCreatedAt',
|
||||
'fileModifiedAt',
|
||||
'height',
|
||||
'id',
|
||||
'isEdited',
|
||||
'isFavorite',
|
||||
'libraryId',
|
||||
'livePhotoVideoId',
|
||||
|
||||
@@ -44,7 +44,7 @@ SyncAssetV1 _createAsset({
|
||||
livePhotoVideoId: null,
|
||||
stackId: null,
|
||||
thumbhash: null,
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
4
mobile/test/drift/main/generated/schema.dart
generated
4
mobile/test/drift/main/generated/schema.dart
generated
@@ -19,6 +19,7 @@ import 'schema_v13.dart' as v13;
|
||||
import 'schema_v14.dart' as v14;
|
||||
import 'schema_v15.dart' as v15;
|
||||
import 'schema_v16.dart' as v16;
|
||||
import 'schema_v17.dart' as v17;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
@@ -56,6 +57,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
return v15.DatabaseAtV15(db);
|
||||
case 16:
|
||||
return v16.DatabaseAtV16(db);
|
||||
case 17:
|
||||
return v17.DatabaseAtV17(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
@@ -78,5 +81,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
];
|
||||
}
|
||||
|
||||
8337
mobile/test/drift/main/generated/schema_v17.dart
generated
Normal file
8337
mobile/test/drift/main/generated/schema_v17.dart
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
mobile/test/fixtures/asset.stub.dart
vendored
2
mobile/test/fixtures/asset.stub.dart
vendored
@@ -64,6 +64,7 @@ abstract final class LocalAssetStub {
|
||||
type: AssetType.image,
|
||||
createdAt: DateTime(2025),
|
||||
updatedAt: DateTime(2025, 2),
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
static final image2 = LocalAsset(
|
||||
@@ -72,5 +73,6 @@ abstract final class LocalAssetStub {
|
||||
type: AssetType.image,
|
||||
createdAt: DateTime(2000),
|
||||
updatedAt: DateTime(20021),
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
2
mobile/test/fixtures/sync_stream.stub.dart
vendored
2
mobile/test/fixtures/sync_stream.stub.dart
vendored
@@ -128,7 +128,7 @@ abstract final class SyncStreamStub {
|
||||
visibility: AssetVisibility.timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
),
|
||||
ack: ack,
|
||||
);
|
||||
|
||||
@@ -194,6 +194,7 @@ void main() {
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
adjustmentTime: DateTime(2026, 1, 2),
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
final mockEntity = MockAssetEntity();
|
||||
@@ -242,6 +243,7 @@ void main() {
|
||||
cloudId: 'cloud-id-123',
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
final mockEntity = MockAssetEntity();
|
||||
@@ -279,6 +281,7 @@ void main() {
|
||||
createdAt: DateTime(2025, 1, 1),
|
||||
updatedAt: DateTime(2025, 1, 2),
|
||||
cloudId: null, // No cloudId
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
final mockEntity = MockAssetEntity();
|
||||
@@ -320,6 +323,7 @@ void main() {
|
||||
cloudId: 'cloud-id-livephoto',
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
final mockEntity = MockAssetEntity();
|
||||
|
||||
@@ -131,6 +131,7 @@ abstract final class TestUtils {
|
||||
isFavorite: false,
|
||||
width: width,
|
||||
height: height,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,6 +155,7 @@ abstract final class TestUtils {
|
||||
width: width,
|
||||
height: height,
|
||||
orientation: orientation,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class MediumFactory {
|
||||
type: type ?? AssetType.image,
|
||||
createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)),
|
||||
updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)),
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ LocalAsset createLocalAsset({
|
||||
createdAt: createdAt ?? DateTime.now(),
|
||||
updatedAt: updatedAt ?? DateTime.now(),
|
||||
isFavorite: isFavorite,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ RemoteAsset createRemoteAsset({
|
||||
createdAt: createdAt ?? DateTime.now(),
|
||||
updatedAt: updatedAt ?? DateTime.now(),
|
||||
isFavorite: isFavorite,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21291,9 +21291,6 @@
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"editCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"fileCreatedAt": {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
@@ -21311,6 +21308,9 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isEdited": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isFavorite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -21364,11 +21364,11 @@
|
||||
"checksum",
|
||||
"deletedAt",
|
||||
"duration",
|
||||
"editCount",
|
||||
"fileCreatedAt",
|
||||
"fileModifiedAt",
|
||||
"height",
|
||||
"id",
|
||||
"isEdited",
|
||||
"isFavorite",
|
||||
"libraryId",
|
||||
"livePhotoVideoId",
|
||||
|
||||
@@ -11,6 +11,7 @@ packages:
|
||||
- .github
|
||||
ignoredBuiltDependencies:
|
||||
- '@nestjs/core'
|
||||
- '@parcel/watcher'
|
||||
- '@scarf/scarf'
|
||||
- '@swc/core'
|
||||
- canvas
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { StorageAsset } from 'src/database';
|
||||
import {
|
||||
AssetFileType,
|
||||
AssetPathType,
|
||||
ImageFormat,
|
||||
PathType,
|
||||
PersonPathType,
|
||||
RawExtractedFormat,
|
||||
StorageFolder,
|
||||
} from 'src/enum';
|
||||
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
@@ -32,6 +24,15 @@ export interface MoveRequest {
|
||||
};
|
||||
}
|
||||
|
||||
export type GeneratedImageType =
|
||||
| AssetPathType.Preview
|
||||
| AssetPathType.Thumbnail
|
||||
| AssetPathType.FullSize
|
||||
| AssetPathType.EditedPreview
|
||||
| AssetPathType.EditedThumbnail
|
||||
| AssetPathType.EditedFullSize;
|
||||
export type GeneratedAssetType = GeneratedImageType | AssetPathType.EncodedVideo;
|
||||
|
||||
export type ThumbnailPathEntity = { id: string; ownerId: string };
|
||||
|
||||
let instance: StorageCore | null;
|
||||
@@ -110,19 +111,8 @@ export class StorageCore {
|
||||
return StorageCore.getNestedPath(StorageFolder.Thumbnails, person.ownerId, `${person.id}.jpeg`);
|
||||
}
|
||||
|
||||
static getImagePath(
|
||||
asset: ThumbnailPathEntity,
|
||||
{
|
||||
fileType,
|
||||
format,
|
||||
isEdited,
|
||||
}: { fileType: AssetFileType; format: ImageFormat | RawExtractedFormat; isEdited: boolean },
|
||||
) {
|
||||
return StorageCore.getNestedPath(
|
||||
StorageFolder.Thumbnails,
|
||||
asset.ownerId,
|
||||
`${asset.id}_${fileType}${isEdited ? '_edited' : ''}.${format}`,
|
||||
);
|
||||
static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: 'jpeg' | 'webp') {
|
||||
return StorageCore.getNestedPath(StorageFolder.Thumbnails, asset.ownerId, `${asset.id}-${type}.${format}`);
|
||||
}
|
||||
|
||||
static getEncodedVideoPath(asset: ThumbnailPathEntity) {
|
||||
@@ -147,14 +137,14 @@ export class StorageCore {
|
||||
return normalizedPath.startsWith(normalizedAppMediaLocation);
|
||||
}
|
||||
|
||||
async moveAssetImage(asset: StorageAsset, fileType: AssetFileType, format: ImageFormat) {
|
||||
async moveAssetImage(asset: StorageAsset, pathType: GeneratedImageType, format: ImageFormat) {
|
||||
const { id: entityId, files } = asset;
|
||||
const oldFile = getAssetFile(files, fileType, { isEdited: false });
|
||||
const oldFile = getAssetFile(files, pathType);
|
||||
return this.moveFile({
|
||||
entityId,
|
||||
pathType: fileType,
|
||||
pathType,
|
||||
oldPath: oldFile?.path || null,
|
||||
newPath: StorageCore.getImagePath(asset, { fileType, format, isEdited: false }),
|
||||
newPath: StorageCore.getImagePath(asset, pathType, format),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -308,19 +298,19 @@ export class StorageCore {
|
||||
case AssetPathType.Original: {
|
||||
return this.assetRepository.update({ id, originalPath: newPath });
|
||||
}
|
||||
case AssetFileType.FullSize: {
|
||||
case AssetPathType.FullSize: {
|
||||
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FullSize, path: newPath });
|
||||
}
|
||||
case AssetFileType.Preview: {
|
||||
case AssetPathType.Preview: {
|
||||
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Preview, path: newPath });
|
||||
}
|
||||
case AssetFileType.Thumbnail: {
|
||||
case AssetPathType.Thumbnail: {
|
||||
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Thumbnail, path: newPath });
|
||||
}
|
||||
case AssetPathType.EncodedVideo: {
|
||||
return this.assetRepository.update({ id, encodedVideoPath: newPath });
|
||||
}
|
||||
case AssetFileType.Sidecar: {
|
||||
case AssetPathType.Sidecar: {
|
||||
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Sidecar, path: newPath });
|
||||
}
|
||||
case PersonPathType.Face: {
|
||||
|
||||
@@ -39,7 +39,6 @@ export type AssetFile = {
|
||||
id: string;
|
||||
type: AssetFileType;
|
||||
path: string;
|
||||
isEdited: boolean;
|
||||
};
|
||||
|
||||
export type Library = {
|
||||
@@ -345,7 +344,7 @@ export const columns = {
|
||||
'asset.width',
|
||||
'asset.height',
|
||||
],
|
||||
assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type', 'asset_file.isEdited'],
|
||||
assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'],
|
||||
authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'],
|
||||
authApiKey: ['api_key.id', 'api_key.permissions'],
|
||||
authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt', 'session.appVersion'],
|
||||
@@ -396,7 +395,7 @@ export const columns = {
|
||||
'asset.libraryId',
|
||||
'asset.width',
|
||||
'asset.height',
|
||||
'asset.editCount',
|
||||
'asset.isEdited',
|
||||
],
|
||||
syncAlbumUser: ['album_user.albumId as albumId', 'album_user.userId as userId', 'album_user.role'],
|
||||
syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'],
|
||||
|
||||
@@ -139,7 +139,7 @@ export type MapAsset = {
|
||||
type: AssetType;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
editCount: number;
|
||||
isEdited: boolean;
|
||||
};
|
||||
|
||||
export class AssetStackResponseDto {
|
||||
@@ -248,6 +248,6 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
||||
resized: true,
|
||||
width: entity.width,
|
||||
height: entity.height,
|
||||
isEdited: entity.editCount > 0,
|
||||
isEdited: entity.isEdited,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,8 +121,8 @@ export class SyncAssetV1 {
|
||||
width!: number | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
height!: number | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
editCount!: number;
|
||||
@ApiProperty({ type: 'boolean' })
|
||||
isEdited!: boolean;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
|
||||
@@ -45,6 +45,9 @@ export enum AssetFileType {
|
||||
Preview = 'preview',
|
||||
Thumbnail = 'thumbnail',
|
||||
Sidecar = 'sidecar',
|
||||
FullSizeEdited = 'fullsize_edited',
|
||||
PreviewEdited = 'preview_edited',
|
||||
ThumbnailEdited = 'thumbnail_edited',
|
||||
}
|
||||
|
||||
export enum AlbumUserRole {
|
||||
@@ -361,7 +364,14 @@ export enum ManualJobName {
|
||||
|
||||
export enum AssetPathType {
|
||||
Original = 'original',
|
||||
FullSize = 'fullsize',
|
||||
Preview = 'preview',
|
||||
EditedFullSize = 'edited_fullsize',
|
||||
EditedPreview = 'edited_preview',
|
||||
EditedThumbnail = 'edited_thumbnail',
|
||||
Thumbnail = 'thumbnail',
|
||||
EncodedVideo = 'encoded_video',
|
||||
Sidecar = 'sidecar',
|
||||
}
|
||||
|
||||
export enum PersonPathType {
|
||||
@@ -372,7 +382,7 @@ export enum UserPathType {
|
||||
Profile = 'profile',
|
||||
}
|
||||
|
||||
export type PathType = AssetFileType | AssetPathType | PersonPathType | UserPathType;
|
||||
export type PathType = AssetPathType | PersonPathType | UserPathType;
|
||||
|
||||
export enum TranscodePolicy {
|
||||
All = 'all',
|
||||
|
||||
@@ -29,8 +29,7 @@ select
|
||||
select
|
||||
"asset_file"."id",
|
||||
"asset_file"."path",
|
||||
"asset_file"."type",
|
||||
"asset_file"."isEdited"
|
||||
"asset_file"."type"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
@@ -73,8 +72,7 @@ select
|
||||
select
|
||||
"asset_file"."id",
|
||||
"asset_file"."path",
|
||||
"asset_file"."type",
|
||||
"asset_file"."isEdited"
|
||||
"asset_file"."type"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
@@ -101,8 +99,7 @@ select
|
||||
select
|
||||
"asset_file"."id",
|
||||
"asset_file"."path",
|
||||
"asset_file"."type",
|
||||
"asset_file"."isEdited"
|
||||
"asset_file"."type"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
@@ -148,8 +145,7 @@ select
|
||||
select
|
||||
"asset_file"."id",
|
||||
"asset_file"."path",
|
||||
"asset_file"."type",
|
||||
"asset_file"."isEdited"
|
||||
"asset_file"."type"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
@@ -178,8 +174,7 @@ select
|
||||
select
|
||||
"asset_file"."id",
|
||||
"asset_file"."path",
|
||||
"asset_file"."type",
|
||||
"asset_file"."isEdited"
|
||||
"asset_file"."type"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
@@ -249,8 +244,7 @@ select
|
||||
select
|
||||
"asset_file"."id",
|
||||
"asset_file"."path",
|
||||
"asset_file"."type",
|
||||
"asset_file"."isEdited"
|
||||
"asset_file"."type"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
@@ -275,8 +269,7 @@ where
|
||||
select
|
||||
"asset_file"."id",
|
||||
"asset_file"."path",
|
||||
"asset_file"."type",
|
||||
"asset_file"."isEdited"
|
||||
"asset_file"."type"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
@@ -325,8 +318,7 @@ select
|
||||
select
|
||||
"asset_file"."id",
|
||||
"asset_file"."path",
|
||||
"asset_file"."type",
|
||||
"asset_file"."isEdited"
|
||||
"asset_file"."type"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
@@ -365,8 +357,7 @@ select
|
||||
select
|
||||
"asset_file"."id",
|
||||
"asset_file"."path",
|
||||
"asset_file"."type",
|
||||
"asset_file"."isEdited"
|
||||
"asset_file"."type"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
@@ -453,8 +444,7 @@ select
|
||||
select
|
||||
"asset_file"."id",
|
||||
"asset_file"."path",
|
||||
"asset_file"."type",
|
||||
"asset_file"."isEdited"
|
||||
"asset_file"."type"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
@@ -546,8 +536,7 @@ select
|
||||
select
|
||||
"asset_file"."id",
|
||||
"asset_file"."path",
|
||||
"asset_file"."type",
|
||||
"asset_file"."isEdited"
|
||||
"asset_file"."type"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
@@ -586,8 +575,7 @@ select
|
||||
select
|
||||
"asset_file"."id",
|
||||
"asset_file"."path",
|
||||
"asset_file"."type",
|
||||
"asset_file"."isEdited"
|
||||
"asset_file"."type"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
|
||||
@@ -286,8 +286,7 @@ select
|
||||
select
|
||||
"asset_file"."id",
|
||||
"asset_file"."path",
|
||||
"asset_file"."type",
|
||||
"asset_file"."isEdited"
|
||||
"asset_file"."type"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
|
||||
@@ -71,7 +71,7 @@ select
|
||||
"asset"."libraryId",
|
||||
"asset"."width",
|
||||
"asset"."height",
|
||||
"asset"."editCount",
|
||||
"asset"."isEdited",
|
||||
"album_asset"."updateId"
|
||||
from
|
||||
"album_asset" as "album_asset"
|
||||
@@ -104,7 +104,7 @@ select
|
||||
"asset"."libraryId",
|
||||
"asset"."width",
|
||||
"asset"."height",
|
||||
"asset"."editCount",
|
||||
"asset"."isEdited",
|
||||
"asset"."updateId"
|
||||
from
|
||||
"asset" as "asset"
|
||||
@@ -143,7 +143,7 @@ select
|
||||
"asset"."libraryId",
|
||||
"asset"."width",
|
||||
"asset"."height",
|
||||
"asset"."editCount"
|
||||
"asset"."isEdited"
|
||||
from
|
||||
"album_asset" as "album_asset"
|
||||
inner join "asset" on "asset"."id" = "album_asset"."assetId"
|
||||
@@ -459,7 +459,7 @@ select
|
||||
"asset"."libraryId",
|
||||
"asset"."width",
|
||||
"asset"."height",
|
||||
"asset"."editCount",
|
||||
"asset"."isEdited",
|
||||
"asset"."updateId"
|
||||
from
|
||||
"asset" as "asset"
|
||||
@@ -755,7 +755,7 @@ select
|
||||
"asset"."libraryId",
|
||||
"asset"."width",
|
||||
"asset"."height",
|
||||
"asset"."editCount",
|
||||
"asset"."isEdited",
|
||||
"asset"."updateId"
|
||||
from
|
||||
"asset" as "asset"
|
||||
@@ -807,7 +807,7 @@ select
|
||||
"asset"."libraryId",
|
||||
"asset"."width",
|
||||
"asset"."height",
|
||||
"asset"."editCount",
|
||||
"asset"."isEdited",
|
||||
"asset"."updateId"
|
||||
from
|
||||
"asset" as "asset"
|
||||
|
||||
@@ -903,22 +903,20 @@ export class AssetRepository {
|
||||
.execute();
|
||||
}
|
||||
|
||||
async upsertFile(file: Pick<Insertable<AssetFileTable>, 'assetId' | 'path' | 'type' | 'isEdited'>): Promise<void> {
|
||||
async upsertFile(file: Pick<Insertable<AssetFileTable>, 'assetId' | 'path' | 'type'>): Promise<void> {
|
||||
const value = { ...file, assetId: asUuid(file.assetId) };
|
||||
await this.db
|
||||
.insertInto('asset_file')
|
||||
.values(value)
|
||||
.onConflict((oc) =>
|
||||
oc.columns(['assetId', 'type', 'isEdited']).doUpdateSet((eb) => ({
|
||||
oc.columns(['assetId', 'type']).doUpdateSet((eb) => ({
|
||||
path: eb.ref('excluded.path'),
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async upsertFiles(
|
||||
files: Pick<Insertable<AssetFileTable>, 'assetId' | 'path' | 'type' | 'isEdited'>[],
|
||||
): Promise<void> {
|
||||
async upsertFiles(files: Pick<Insertable<AssetFileTable>, 'assetId' | 'path' | 'type'>[]): Promise<void> {
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -928,7 +926,7 @@ export class AssetRepository {
|
||||
.insertInto('asset_file')
|
||||
.values(values)
|
||||
.onConflict((oc) =>
|
||||
oc.columns(['assetId', 'type', 'isEdited']).doUpdateSet((eb) => ({
|
||||
oc.columns(['assetId', 'type']).doUpdateSet((eb) => ({
|
||||
path: eb.ref('excluded.path'),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface ClientEventMap {
|
||||
|
||||
AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }];
|
||||
AppRestartV1: [AppRestartEvent];
|
||||
AssetEditReadyV1: [{ assetId: string }];
|
||||
AssetEditReadyV1: [{ asset: SyncAssetV1 }];
|
||||
}
|
||||
|
||||
export type AuthFn = (client: Socket) => Promise<AuthDto>;
|
||||
|
||||
@@ -263,8 +263,9 @@ export const asset_edit_insert = registerFunction({
|
||||
body: `
|
||||
BEGIN
|
||||
UPDATE asset
|
||||
SET "editCount" = "editCount" + 1
|
||||
WHERE "id" = NEW."assetId";
|
||||
SET "isEdited" = true
|
||||
FROM inserted_edit
|
||||
WHERE asset.id = inserted_edit."assetId" AND NOT asset."isEdited";
|
||||
RETURN NULL;
|
||||
END
|
||||
`,
|
||||
@@ -277,8 +278,10 @@ export const asset_edit_delete = registerFunction({
|
||||
body: `
|
||||
BEGIN
|
||||
UPDATE asset
|
||||
SET "editCount" = "editCount" - 1
|
||||
WHERE "id" = OLD."assetId";
|
||||
SET "isEdited" = false
|
||||
FROM deleted_edit
|
||||
WHERE asset.id = deleted_edit."assetId" AND asset."isEdited"
|
||||
AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id);
|
||||
RETURN NULL;
|
||||
END
|
||||
`,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE OR REPLACE FUNCTION asset_edit_insert()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
UPDATE asset
|
||||
SET "isEdited" = true
|
||||
FROM inserted_edit
|
||||
WHERE asset.id = inserted_edit."assetId" AND NOT asset."isEdited";
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`CREATE OR REPLACE FUNCTION asset_edit_delete()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
UPDATE asset
|
||||
SET "isEdited" = false
|
||||
FROM deleted_edit
|
||||
WHERE asset.id = deleted_edit."assetId" AND asset."isEdited"
|
||||
AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id);
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`ALTER TABLE "asset" ADD "isEdited" boolean NOT NULL DEFAULT false;`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "asset_edit_delete"
|
||||
AFTER DELETE ON "asset_edit"
|
||||
REFERENCING OLD TABLE AS "deleted_edit"
|
||||
FOR EACH STATEMENT
|
||||
WHEN (pg_trigger_depth() = 0)
|
||||
EXECUTE FUNCTION asset_edit_delete();`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "asset_edit_insert"
|
||||
AFTER INSERT ON "asset_edit"
|
||||
REFERENCING NEW TABLE AS "inserted_edit"
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION asset_edit_insert();`.execute(db);
|
||||
await sql`ALTER TABLE "asset" DROP COLUMN "editCount";`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"asset_edit_insert","sql":"CREATE OR REPLACE FUNCTION asset_edit_insert()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = true\\n FROM inserted_edit\\n WHERE asset.id = inserted_edit.\\"assetId\\" AND NOT asset.\\"isEdited\\";\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_asset_edit_insert';`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"asset_edit_delete","sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = false\\n FROM deleted_edit\\n WHERE asset.id = deleted_edit.\\"assetId\\" AND asset.\\"isEdited\\" \\n AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit.\\"assetId\\" = asset.id);\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"trigger","name":"asset_edit_delete","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_delete\\"\\n AFTER DELETE ON \\"asset_edit\\"\\n REFERENCING OLD TABLE AS \\"deleted_edit\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_edit_delete();"}'::jsonb WHERE "name" = 'trigger_asset_edit_delete';`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"trigger","name":"asset_edit_insert","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_insert\\"\\n AFTER INSERT ON \\"asset_edit\\"\\n REFERENCING NEW TABLE AS \\"inserted_edit\\"\\n FOR EACH STATEMENT\\n EXECUTE FUNCTION asset_edit_insert();"}'::jsonb WHERE "name" = 'trigger_asset_edit_insert';`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE OR REPLACE FUNCTION public.asset_edit_insert()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
UPDATE asset
|
||||
SET "editCount" = "editCount" + 1
|
||||
WHERE "id" = NEW."assetId";
|
||||
RETURN NULL;
|
||||
END
|
||||
$function$
|
||||
`.execute(db);
|
||||
await sql`CREATE OR REPLACE FUNCTION public.asset_edit_delete()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
UPDATE asset
|
||||
SET "editCount" = "editCount" - 1
|
||||
WHERE "id" = OLD."assetId";
|
||||
RETURN NULL;
|
||||
END
|
||||
$function$
|
||||
`.execute(db);
|
||||
await sql`ALTER TABLE "asset" ADD "editCount" integer NOT NULL DEFAULT 0;`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "asset_edit_delete"
|
||||
AFTER DELETE ON "asset_edit"
|
||||
REFERENCING OLD TABLE AS "old"
|
||||
FOR EACH ROW
|
||||
WHEN ((pg_trigger_depth() = 0))
|
||||
EXECUTE FUNCTION asset_edit_delete();`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "asset_edit_insert"
|
||||
AFTER INSERT ON "asset_edit"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION asset_edit_insert();`.execute(db);
|
||||
await sql`ALTER TABLE "asset" DROP COLUMN "isEdited";`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE FUNCTION asset_edit_insert()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"editCount\\" = \\"editCount\\" + 1\\n WHERE \\"id\\" = NEW.\\"assetId\\";\\n RETURN NULL;\\n END\\n $$;","name":"asset_edit_insert","type":"function"}'::jsonb WHERE "name" = 'function_asset_edit_insert';`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"editCount\\" = \\"editCount\\" - 1\\n WHERE \\"id\\" = OLD.\\"assetId\\";\\n RETURN NULL;\\n END\\n $$;","name":"asset_edit_delete","type":"function"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_delete\\"\\n AFTER DELETE ON \\"asset_edit\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH ROW\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_edit_delete();","name":"asset_edit_delete","type":"trigger"}'::jsonb WHERE "name" = 'trigger_asset_edit_delete';`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_insert\\"\\n AFTER INSERT ON \\"asset_edit\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION asset_edit_insert();","name":"asset_edit_insert","type":"trigger"}'::jsonb WHERE "name" = 'trigger_asset_edit_insert';`.execute(db);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "asset_file" DROP CONSTRAINT "asset_file_assetId_type_uq";`.execute(db);
|
||||
await sql`ALTER TABLE "asset_file" ADD "isEdited" boolean NOT NULL DEFAULT false;`.execute(db);
|
||||
await sql`ALTER TABLE "asset_file" ADD CONSTRAINT "asset_file_assetId_type_isEdited_uq" UNIQUE ("assetId", "type", "isEdited");`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "asset_file" DROP CONSTRAINT "asset_file_assetId_type_isEdited_uq";`.execute(db);
|
||||
await sql`ALTER TABLE "asset_file" ADD CONSTRAINT "asset_file_assetId_type_uq" UNIQUE ("assetId", "type");`.execute(db);
|
||||
await sql`ALTER TABLE "asset_file" DROP COLUMN "isEdited";`.execute(db);
|
||||
}
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table('asset_edit')
|
||||
@AfterInsertTrigger({ scope: 'row', function: asset_edit_insert })
|
||||
@AfterInsertTrigger({ scope: 'statement', function: asset_edit_insert, referencingNewTableAs: 'inserted_edit' })
|
||||
@AfterDeleteTrigger({
|
||||
scope: 'row',
|
||||
scope: 'statement',
|
||||
function: asset_edit_delete,
|
||||
referencingOldTableAs: 'old',
|
||||
referencingOldTableAs: 'deleted_edit',
|
||||
when: 'pg_trigger_depth() = 0',
|
||||
})
|
||||
export class AssetEditTable<T extends AssetEditAction = AssetEditAction> {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table('asset_file')
|
||||
@Unique({ columns: ['assetId', 'type', 'isEdited'] })
|
||||
@Unique({ columns: ['assetId', 'type'] })
|
||||
@UpdatedAtTrigger('asset_file_updatedAt')
|
||||
export class AssetFileTable {
|
||||
@PrimaryGeneratedColumn()
|
||||
@@ -37,7 +37,4 @@ export class AssetFileTable {
|
||||
|
||||
@UpdateIdColumn({ index: true })
|
||||
updateId!: Generated<string>;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isEdited!: Generated<boolean>;
|
||||
}
|
||||
|
||||
@@ -144,6 +144,6 @@ export class AssetTable {
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
height!: number | null;
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
editCount!: Generated<number>;
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isEdited!: Generated<boolean>;
|
||||
}
|
||||
|
||||
@@ -107,6 +107,78 @@ describe(ApiKeyService.name, () => {
|
||||
permissions: newPermissions,
|
||||
});
|
||||
});
|
||||
|
||||
describe('api key auth', () => {
|
||||
it('should prevent adding Permission.all', async () => {
|
||||
const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead];
|
||||
const auth = factory.auth({ apiKey: { permissions } });
|
||||
const apiKey = factory.apiKey({ userId: auth.user.id, permissions });
|
||||
|
||||
mocks.apiKey.getById.mockResolvedValue(apiKey);
|
||||
|
||||
await expect(sut.update(auth, apiKey.id, { permissions: [Permission.All] })).rejects.toThrow(
|
||||
'Cannot grant permissions you do not have',
|
||||
);
|
||||
|
||||
expect(mocks.apiKey.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent adding a new permission', async () => {
|
||||
const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead];
|
||||
const auth = factory.auth({ apiKey: { permissions } });
|
||||
const apiKey = factory.apiKey({ userId: auth.user.id, permissions });
|
||||
|
||||
mocks.apiKey.getById.mockResolvedValue(apiKey);
|
||||
|
||||
await expect(sut.update(auth, apiKey.id, { permissions: [Permission.AssetCopy] })).rejects.toThrow(
|
||||
'Cannot grant permissions you do not have',
|
||||
);
|
||||
|
||||
expect(mocks.apiKey.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow removing permissions', async () => {
|
||||
const auth = factory.auth({ apiKey: { permissions: [Permission.ApiKeyUpdate, Permission.AssetRead] } });
|
||||
const apiKey = factory.apiKey({
|
||||
userId: auth.user.id,
|
||||
permissions: [Permission.AssetRead, Permission.AssetDelete],
|
||||
});
|
||||
|
||||
mocks.apiKey.getById.mockResolvedValue(apiKey);
|
||||
mocks.apiKey.update.mockResolvedValue(apiKey);
|
||||
|
||||
// remove Permission.AssetDelete
|
||||
await sut.update(auth, apiKey.id, { permissions: [Permission.AssetRead] });
|
||||
|
||||
expect(mocks.apiKey.update).toHaveBeenCalledWith(
|
||||
auth.user.id,
|
||||
apiKey.id,
|
||||
expect.objectContaining({ permissions: [Permission.AssetRead] }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow adding new permissions', async () => {
|
||||
const auth = factory.auth({
|
||||
apiKey: { permissions: [Permission.ApiKeyUpdate, Permission.AssetRead, Permission.AssetUpdate] },
|
||||
});
|
||||
const apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.AssetRead] });
|
||||
|
||||
mocks.apiKey.getById.mockResolvedValue(apiKey);
|
||||
mocks.apiKey.update.mockResolvedValue(apiKey);
|
||||
|
||||
// add Permission.AssetUpdate
|
||||
await sut.update(auth, apiKey.id, {
|
||||
name: apiKey.name,
|
||||
permissions: [Permission.AssetRead, Permission.AssetUpdate],
|
||||
});
|
||||
|
||||
expect(mocks.apiKey.update).toHaveBeenCalledWith(
|
||||
auth.user.id,
|
||||
apiKey.id,
|
||||
expect.objectContaining({ permissions: [Permission.AssetRead, Permission.AssetUpdate] }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
|
||||
@@ -32,6 +32,14 @@ export class ApiKeyService extends BaseService {
|
||||
throw new BadRequestException('API Key not found');
|
||||
}
|
||||
|
||||
if (
|
||||
auth.apiKey &&
|
||||
dto.permissions &&
|
||||
!isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })
|
||||
) {
|
||||
throw new BadRequestException('Cannot grant permissions you do not have');
|
||||
}
|
||||
|
||||
const key = await this.apiKeyRepository.update(auth.user.id, id, { name: dto.name, permissions: dto.permissions });
|
||||
|
||||
return this.map(key);
|
||||
|
||||
@@ -529,10 +529,9 @@ describe(AssetMediaService.name, () => {
|
||||
...assetStub.withCropEdit.files,
|
||||
{
|
||||
id: 'edited-file',
|
||||
type: AssetFileType.FullSize,
|
||||
type: AssetFileType.FullSizeEdited,
|
||||
path: '/uploads/user-id/fullsize/edited.jpg',
|
||||
isEdited: true,
|
||||
},
|
||||
} as AssetFile,
|
||||
],
|
||||
};
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
@@ -555,10 +554,9 @@ describe(AssetMediaService.name, () => {
|
||||
...assetStub.withCropEdit.files,
|
||||
{
|
||||
id: 'edited-file',
|
||||
type: AssetFileType.FullSize,
|
||||
type: AssetFileType.FullSizeEdited,
|
||||
path: '/uploads/user-id/fullsize/edited.jpg',
|
||||
isEdited: true,
|
||||
},
|
||||
} as AssetFile,
|
||||
],
|
||||
};
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
@@ -581,10 +579,9 @@ describe(AssetMediaService.name, () => {
|
||||
...assetStub.withCropEdit.files,
|
||||
{
|
||||
id: 'edited-file',
|
||||
type: AssetFileType.FullSize,
|
||||
type: AssetFileType.FullSizeEdited,
|
||||
path: '/uploads/user-id/fullsize/edited.jpg',
|
||||
isEdited: true,
|
||||
},
|
||||
} as AssetFile,
|
||||
],
|
||||
};
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
@@ -659,7 +656,6 @@ describe(AssetMediaService.name, () => {
|
||||
id: '42',
|
||||
path: '/path/to/preview',
|
||||
type: AssetFileType.Thumbnail,
|
||||
isEdited: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -677,7 +673,6 @@ describe(AssetMediaService.name, () => {
|
||||
id: '42',
|
||||
path: '/path/to/preview.jpg',
|
||||
type: AssetFileType.Preview,
|
||||
isEdited: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DateTime, Duration } from 'luxon';
|
||||
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||
import { AssetFile } from 'src/database';
|
||||
import { OnJob } from 'src/decorators';
|
||||
import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetResponseDto, MapAsset, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import {
|
||||
AssetBulkDeleteDto,
|
||||
AssetBulkUpdateDto,
|
||||
@@ -112,7 +112,7 @@ export class AssetService extends BaseService {
|
||||
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
||||
const repos = { asset: this.assetRepository, event: this.eventRepository };
|
||||
|
||||
let previousMotion: { id: string } | null = null;
|
||||
let previousMotion: MapAsset | null = null;
|
||||
if (rest.livePhotoVideoId) {
|
||||
await onBeforeLink(repos, { userId: auth.user.id, livePhotoVideoId: rest.livePhotoVideoId });
|
||||
} else if (rest.livePhotoVideoId === null) {
|
||||
|
||||
@@ -100,7 +100,29 @@ export class JobService extends BaseService {
|
||||
const asset = await this.assetRepository.getById(item.data.id);
|
||||
|
||||
if (asset) {
|
||||
this.websocketRepository.clientSend('AssetEditReadyV1', asset.ownerId, { assetId: item.data.id });
|
||||
this.websocketRepository.clientSend('AssetEditReadyV1', asset.ownerId, {
|
||||
asset: {
|
||||
id: asset.id,
|
||||
ownerId: asset.ownerId,
|
||||
originalFileName: asset.originalFileName,
|
||||
thumbhash: asset.thumbhash ? hexOrBufferToBase64(asset.thumbhash) : null,
|
||||
checksum: hexOrBufferToBase64(asset.checksum),
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
localDateTime: asset.localDateTime,
|
||||
duration: asset.duration,
|
||||
type: asset.type,
|
||||
deletedAt: asset.deletedAt,
|
||||
isFavorite: asset.isFavorite,
|
||||
visibility: asset.visibility,
|
||||
livePhotoVideoId: asset.livePhotoVideoId,
|
||||
stackId: asset.stackId,
|
||||
libraryId: asset.libraryId,
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
isEdited: asset.isEdited,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -153,7 +175,7 @@ export class JobService extends BaseService {
|
||||
libraryId: asset.libraryId,
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
editCount: asset.editCount,
|
||||
isEdited: asset.isEdited,
|
||||
},
|
||||
exif: {
|
||||
assetId: exif.assetId,
|
||||
|
||||
@@ -241,21 +241,21 @@ describe(MediaService.name, () => {
|
||||
await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.Success);
|
||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetFileType.FullSize,
|
||||
pathType: AssetPathType.FullSize,
|
||||
oldPath: '/uploads/user-id/fullsize/path.webp',
|
||||
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_fullsize.jpeg'),
|
||||
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-fullsize.jpeg'),
|
||||
});
|
||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetFileType.Preview,
|
||||
pathType: AssetPathType.Preview,
|
||||
oldPath: '/uploads/user-id/thumbs/path.jpg',
|
||||
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_preview.jpeg'),
|
||||
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-preview.jpeg'),
|
||||
});
|
||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetFileType.Thumbnail,
|
||||
pathType: AssetPathType.Thumbnail,
|
||||
oldPath: '/uploads/user-id/webp/path.ext',
|
||||
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_thumbnail.webp'),
|
||||
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-thumbnail.webp'),
|
||||
});
|
||||
expect(mocks.move.create).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
@@ -385,13 +385,11 @@ describe(MediaService.name, () => {
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Preview,
|
||||
path: expect.any(String),
|
||||
isEdited: false,
|
||||
},
|
||||
{
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: expect.any(String),
|
||||
isEdited: false,
|
||||
},
|
||||
]);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer });
|
||||
@@ -423,13 +421,11 @@ describe(MediaService.name, () => {
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Preview,
|
||||
path: expect.any(String),
|
||||
isEdited: false,
|
||||
},
|
||||
{
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: expect.any(String),
|
||||
isEdited: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -460,13 +456,11 @@ describe(MediaService.name, () => {
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Preview,
|
||||
path: expect.any(String),
|
||||
isEdited: false,
|
||||
},
|
||||
{
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: expect.any(String),
|
||||
isEdited: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -554,8 +548,8 @@ describe(MediaService.name, () => {
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
const previewPath = `/data/thumbs/user-id/as/se/asset-id_preview.${format}`;
|
||||
const thumbnailPath = `/data/thumbs/user-id/as/se/asset-id_thumbnail.webp`;
|
||||
const previewPath = `/data/thumbs/user-id/as/se/asset-id-preview.${format}`;
|
||||
const thumbnailPath = `/data/thumbs/user-id/as/se/asset-id-thumbnail.webp`;
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
|
||||
@@ -601,8 +595,8 @@ describe(MediaService.name, () => {
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
const previewPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id_preview.jpeg`);
|
||||
const thumbnailPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id_thumbnail.${format}`);
|
||||
const previewPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id-preview.jpeg`);
|
||||
const thumbnailPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id-thumbnail.${format}`);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
|
||||
@@ -1032,9 +1026,9 @@ describe(MediaService.name, () => {
|
||||
|
||||
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ type: AssetFileType.FullSize, isEdited: true }),
|
||||
expect.objectContaining({ type: AssetFileType.Preview, isEdited: true }),
|
||||
expect.objectContaining({ type: AssetFileType.Thumbnail, isEdited: true }),
|
||||
expect.objectContaining({ type: AssetFileType.FullSizeEdited }),
|
||||
expect.objectContaining({ type: AssetFileType.PreviewEdited }),
|
||||
expect.objectContaining({ type: AssetFileType.ThumbnailEdited }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -1104,17 +1098,17 @@ describe(MediaService.name, () => {
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
expect.anything(),
|
||||
expect.stringContaining('preview_edited.jpeg'),
|
||||
expect.stringContaining('edited_preview.jpeg'),
|
||||
);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
expect.anything(),
|
||||
expect.stringContaining('thumbnail_edited.webp'),
|
||||
expect.stringContaining('edited_thumbnail.webp'),
|
||||
);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
expect.anything(),
|
||||
expect.stringContaining('fullsize_edited.jpeg'),
|
||||
expect.stringContaining('edited_fullsize.jpeg'),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3260,13 +3254,13 @@ describe(MediaService.name, () => {
|
||||
};
|
||||
|
||||
await sut['syncFiles'](asset, [
|
||||
{ type: AssetFileType.Preview, newPath: '/new/preview.jpg', isEdited: false },
|
||||
{ type: AssetFileType.Thumbnail, newPath: '/new/thumbnail.jpg', isEdited: false },
|
||||
{ type: AssetFileType.Preview, newPath: '/new/preview.jpg' },
|
||||
{ type: AssetFileType.Thumbnail, newPath: '/new/thumbnail.jpg' },
|
||||
]);
|
||||
|
||||
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([
|
||||
{ assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview, isEdited: false },
|
||||
{ assetId: 'asset-id', path: '/new/thumbnail.jpg', type: AssetFileType.Thumbnail, isEdited: false },
|
||||
{ assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview },
|
||||
{ assetId: 'asset-id', path: '/new/thumbnail.jpg', type: AssetFileType.Thumbnail },
|
||||
]);
|
||||
expect(mocks.asset.deleteFiles).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
@@ -3276,31 +3270,19 @@ describe(MediaService.name, () => {
|
||||
const asset = {
|
||||
id: 'asset-id',
|
||||
files: [
|
||||
{
|
||||
id: 'file-1',
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Preview,
|
||||
path: '/old/preview.jpg',
|
||||
isEdited: false,
|
||||
},
|
||||
{
|
||||
id: 'file-2',
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: '/old/thumbnail.jpg',
|
||||
isEdited: false,
|
||||
},
|
||||
{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' },
|
||||
{ id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' },
|
||||
],
|
||||
};
|
||||
|
||||
await sut['syncFiles'](asset, [
|
||||
{ type: AssetFileType.Preview, newPath: '/new/preview.jpg', isEdited: false },
|
||||
{ type: AssetFileType.Thumbnail, newPath: '/new/thumbnail.jpg', isEdited: false },
|
||||
{ type: AssetFileType.Preview, newPath: '/new/preview.jpg' },
|
||||
{ type: AssetFileType.Thumbnail, newPath: '/new/thumbnail.jpg' },
|
||||
]);
|
||||
|
||||
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([
|
||||
{ assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview, isEdited: false },
|
||||
{ assetId: 'asset-id', path: '/new/thumbnail.jpg', type: AssetFileType.Thumbnail, isEdited: false },
|
||||
{ assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview },
|
||||
{ assetId: 'asset-id', path: '/new/thumbnail.jpg', type: AssetFileType.Thumbnail },
|
||||
]);
|
||||
expect(mocks.asset.deleteFiles).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
@@ -3313,38 +3295,17 @@ describe(MediaService.name, () => {
|
||||
const asset = {
|
||||
id: 'asset-id',
|
||||
files: [
|
||||
{
|
||||
id: 'file-1',
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Preview,
|
||||
path: '/old/preview.jpg',
|
||||
isEdited: false,
|
||||
},
|
||||
{
|
||||
id: 'file-2',
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: '/old/thumbnail.jpg',
|
||||
isEdited: false,
|
||||
},
|
||||
{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' },
|
||||
{ id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' },
|
||||
],
|
||||
};
|
||||
|
||||
await sut['syncFiles'](asset, [
|
||||
{ type: AssetFileType.Preview, isEdited: false },
|
||||
{ type: AssetFileType.Thumbnail, isEdited: false },
|
||||
]);
|
||||
await sut['syncFiles'](asset, [{ type: AssetFileType.Preview }, { type: AssetFileType.Thumbnail }]);
|
||||
|
||||
expect(mocks.asset.upsertFiles).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([
|
||||
{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg', isEdited: false },
|
||||
{
|
||||
id: 'file-2',
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: '/old/thumbnail.jpg',
|
||||
isEdited: false,
|
||||
},
|
||||
{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' },
|
||||
{ id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' },
|
||||
]);
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.FileDelete,
|
||||
@@ -3356,26 +3317,14 @@ describe(MediaService.name, () => {
|
||||
const asset = {
|
||||
id: 'asset-id',
|
||||
files: [
|
||||
{
|
||||
id: 'file-1',
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Preview,
|
||||
path: '/same/preview.jpg',
|
||||
isEdited: false,
|
||||
},
|
||||
{
|
||||
id: 'file-2',
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: '/same/thumbnail.jpg',
|
||||
isEdited: false,
|
||||
},
|
||||
{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/same/preview.jpg' },
|
||||
{ id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/same/thumbnail.jpg' },
|
||||
],
|
||||
};
|
||||
|
||||
await sut['syncFiles'](asset, [
|
||||
{ type: AssetFileType.Preview, newPath: '/same/preview.jpg', isEdited: false },
|
||||
{ type: AssetFileType.Thumbnail, newPath: '/same/thumbnail.jpg', isEdited: false },
|
||||
{ type: AssetFileType.Preview, newPath: '/same/preview.jpg' },
|
||||
{ type: AssetFileType.Thumbnail, newPath: '/same/thumbnail.jpg' },
|
||||
]);
|
||||
|
||||
expect(mocks.asset.upsertFiles).not.toHaveBeenCalled();
|
||||
@@ -3387,41 +3336,23 @@ describe(MediaService.name, () => {
|
||||
const asset = {
|
||||
id: 'asset-id',
|
||||
files: [
|
||||
{
|
||||
id: 'file-1',
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Preview,
|
||||
path: '/old/preview.jpg',
|
||||
isEdited: false,
|
||||
},
|
||||
{
|
||||
id: 'file-2',
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: '/old/thumbnail.jpg',
|
||||
isEdited: false,
|
||||
},
|
||||
{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' },
|
||||
{ id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' },
|
||||
],
|
||||
};
|
||||
|
||||
await sut['syncFiles'](asset, [
|
||||
{ type: AssetFileType.Preview, newPath: '/new/preview.jpg', isEdited: false }, // replace
|
||||
{ type: AssetFileType.Thumbnail, isEdited: false }, // delete
|
||||
{ type: AssetFileType.FullSize, newPath: '/new/fullsize.jpg', isEdited: false }, // new
|
||||
{ type: AssetFileType.Preview, newPath: '/new/preview.jpg' }, // replace
|
||||
{ type: AssetFileType.Thumbnail }, // delete
|
||||
{ type: AssetFileType.FullSize, newPath: '/new/fullsize.jpg' }, // new
|
||||
]);
|
||||
|
||||
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([
|
||||
{ assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview, isEdited: false },
|
||||
{ assetId: 'asset-id', path: '/new/fullsize.jpg', type: AssetFileType.FullSize, isEdited: false },
|
||||
{ assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview },
|
||||
{ assetId: 'asset-id', path: '/new/fullsize.jpg', type: AssetFileType.FullSize },
|
||||
]);
|
||||
expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([
|
||||
{
|
||||
id: 'file-2',
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: '/old/thumbnail.jpg',
|
||||
isEdited: false,
|
||||
},
|
||||
{ id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' },
|
||||
]);
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.FileDelete,
|
||||
@@ -3445,19 +3376,11 @@ describe(MediaService.name, () => {
|
||||
it('should delete non-existent file types when newPath is not provided', async () => {
|
||||
const asset = {
|
||||
id: 'asset-id',
|
||||
files: [
|
||||
{
|
||||
id: 'file-1',
|
||||
assetId: 'asset-id',
|
||||
type: AssetFileType.Preview,
|
||||
path: '/old/preview.jpg',
|
||||
isEdited: false,
|
||||
},
|
||||
],
|
||||
files: [{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' }],
|
||||
};
|
||||
|
||||
await sut['syncFiles'](asset, [
|
||||
{ type: AssetFileType.Thumbnail, isEdited: false }, // file doesn't exist, newPath not provided
|
||||
{ type: AssetFileType.Thumbnail }, // file doesn't exist, newPath not provided
|
||||
]);
|
||||
|
||||
expect(mocks.asset.upsertFiles).not.toHaveBeenCalled();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AssetEditAction, CropParameters } from 'src/dtos/editing.dto';
|
||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||
import {
|
||||
AssetFileType,
|
||||
AssetPathType,
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
AudioCodec,
|
||||
@@ -49,7 +50,6 @@ interface UpsertFileOptions {
|
||||
assetId: string;
|
||||
type: AssetFileType;
|
||||
path: string;
|
||||
isEdited: boolean;
|
||||
}
|
||||
|
||||
type ThumbnailAsset = NonNullable<Awaited<ReturnType<AssetJobRepository['getForGenerateThumbnailJob']>>>;
|
||||
@@ -160,9 +160,9 @@ export class MediaService extends BaseService {
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
|
||||
await this.storageCore.moveAssetImage(asset, AssetFileType.FullSize, image.fullsize.format);
|
||||
await this.storageCore.moveAssetImage(asset, AssetFileType.Preview, image.preview.format);
|
||||
await this.storageCore.moveAssetImage(asset, AssetFileType.Thumbnail, image.thumbnail.format);
|
||||
await this.storageCore.moveAssetImage(asset, AssetPathType.FullSize, image.fullsize.format);
|
||||
await this.storageCore.moveAssetImage(asset, AssetPathType.Preview, image.preview.format);
|
||||
await this.storageCore.moveAssetImage(asset, AssetPathType.Thumbnail, image.thumbnail.format);
|
||||
await this.storageCore.moveAssetVideo(asset);
|
||||
|
||||
return JobStatus.Success;
|
||||
@@ -236,9 +236,9 @@ export class MediaService extends BaseService {
|
||||
}
|
||||
|
||||
await this.syncFiles(asset, [
|
||||
{ type: AssetFileType.Preview, newPath: generated.previewPath, isEdited: false },
|
||||
{ type: AssetFileType.Thumbnail, newPath: generated.thumbnailPath, isEdited: false },
|
||||
{ type: AssetFileType.FullSize, newPath: generated.fullsizePath, isEdited: false },
|
||||
{ type: AssetFileType.Preview, newPath: generated.previewPath },
|
||||
{ type: AssetFileType.Thumbnail, newPath: generated.thumbnailPath },
|
||||
{ type: AssetFileType.FullSize, newPath: generated.fullsizePath },
|
||||
]);
|
||||
|
||||
const editiedGenerated = await this.generateEditedThumbnails(asset);
|
||||
@@ -307,16 +307,16 @@ export class MediaService extends BaseService {
|
||||
|
||||
private async generateImageThumbnails(asset: ThumbnailAsset, useEdits: boolean = false) {
|
||||
const { image } = await this.getConfig({ withCache: true });
|
||||
const previewPath = StorageCore.getImagePath(asset, {
|
||||
fileType: AssetFileType.Preview,
|
||||
isEdited: useEdits,
|
||||
format: image.preview.format,
|
||||
});
|
||||
const thumbnailPath = StorageCore.getImagePath(asset, {
|
||||
fileType: AssetFileType.Thumbnail,
|
||||
isEdited: useEdits,
|
||||
format: image.thumbnail.format,
|
||||
});
|
||||
const previewPath = StorageCore.getImagePath(
|
||||
asset,
|
||||
useEdits ? AssetPathType.EditedPreview : AssetPathType.Preview,
|
||||
image.preview.format,
|
||||
);
|
||||
const thumbnailPath = StorageCore.getImagePath(
|
||||
asset,
|
||||
useEdits ? AssetPathType.EditedThumbnail : AssetPathType.Thumbnail,
|
||||
image.thumbnail.format,
|
||||
);
|
||||
this.storageCore.ensureFolders(previewPath);
|
||||
|
||||
// Handle embedded preview extraction for RAW files
|
||||
@@ -343,11 +343,11 @@ export class MediaService extends BaseService {
|
||||
|
||||
if (convertFullsize) {
|
||||
// convert a new fullsize image from the same source as the thumbnail
|
||||
fullsizePath = StorageCore.getImagePath(asset, {
|
||||
fileType: AssetFileType.FullSize,
|
||||
isEdited: useEdits,
|
||||
format: image.fullsize.format,
|
||||
});
|
||||
fullsizePath = StorageCore.getImagePath(
|
||||
asset,
|
||||
useEdits ? AssetPathType.EditedFullSize : AssetPathType.FullSize,
|
||||
image.fullsize.format,
|
||||
);
|
||||
const fullsizeOptions = {
|
||||
format: image.fullsize.format,
|
||||
quality: image.fullsize.quality,
|
||||
@@ -355,11 +355,7 @@ export class MediaService extends BaseService {
|
||||
};
|
||||
promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath));
|
||||
} else if (generateFullsize && extracted && extracted.format === RawExtractedFormat.Jpeg) {
|
||||
fullsizePath = StorageCore.getImagePath(asset, {
|
||||
fileType: AssetFileType.FullSize,
|
||||
format: extracted.format,
|
||||
isEdited: false,
|
||||
});
|
||||
fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FullSize, extracted.format);
|
||||
this.storageCore.ensureFolders(fullsizePath);
|
||||
|
||||
// Write the buffer to disk with essential EXIF data
|
||||
@@ -493,16 +489,8 @@ export class MediaService extends BaseService {
|
||||
|
||||
private async generateVideoThumbnails(asset: ThumbnailPathEntity & { originalPath: string }) {
|
||||
const { image, ffmpeg } = await this.getConfig({ withCache: true });
|
||||
const previewPath = StorageCore.getImagePath(asset, {
|
||||
fileType: AssetFileType.Preview,
|
||||
format: image.preview.format,
|
||||
isEdited: false,
|
||||
});
|
||||
const thumbnailPath = StorageCore.getImagePath(asset, {
|
||||
fileType: AssetFileType.Thumbnail,
|
||||
format: image.thumbnail.format,
|
||||
isEdited: false,
|
||||
});
|
||||
const previewPath = StorageCore.getImagePath(asset, AssetPathType.Preview, image.preview.format);
|
||||
const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.Thumbnail, image.thumbnail.format);
|
||||
this.storageCore.ensureFolders(previewPath);
|
||||
|
||||
const { format, audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath);
|
||||
@@ -791,18 +779,18 @@ export class MediaService extends BaseService {
|
||||
|
||||
private async syncFiles(
|
||||
asset: { id: string; files: AssetFile[] },
|
||||
files: { type: AssetFileType; newPath?: string; isEdited: boolean }[],
|
||||
files: { type: AssetFileType; newPath?: string }[],
|
||||
) {
|
||||
const toUpsert: UpsertFileOptions[] = [];
|
||||
const pathsToDelete: string[] = [];
|
||||
const toDelete: AssetFile[] = [];
|
||||
|
||||
for (const { type, newPath, isEdited } of files) {
|
||||
const existingFile = asset.files.find((file) => file.type === type && file.isEdited === isEdited);
|
||||
for (const { type, newPath } of files) {
|
||||
const existingFile = asset.files.find((file) => file.type === type);
|
||||
|
||||
// upsert new file path
|
||||
if (newPath && existingFile?.path !== newPath) {
|
||||
toUpsert.push({ assetId: asset.id, path: newPath, type, isEdited });
|
||||
toUpsert.push({ assetId: asset.id, path: newPath, type });
|
||||
|
||||
// delete old file from disk
|
||||
if (existingFile) {
|
||||
@@ -841,9 +829,9 @@ export class MediaService extends BaseService {
|
||||
const generated = asset.edits.length > 0 ? await this.generateImageThumbnails(asset, true) : undefined;
|
||||
|
||||
await this.syncFiles(asset, [
|
||||
{ type: AssetFileType.Preview, newPath: generated?.previewPath, isEdited: true },
|
||||
{ type: AssetFileType.Thumbnail, newPath: generated?.thumbnailPath, isEdited: true },
|
||||
{ type: AssetFileType.FullSize, newPath: generated?.fullsizePath, isEdited: true },
|
||||
{ type: AssetFileType.PreviewEdited, newPath: generated?.previewPath },
|
||||
{ type: AssetFileType.ThumbnailEdited, newPath: generated?.thumbnailPath },
|
||||
{ type: AssetFileType.FullSizeEdited, newPath: generated?.fullsizePath },
|
||||
]);
|
||||
|
||||
const crop = asset.edits.find((e) => e.action === AssetEditAction.Crop);
|
||||
|
||||
@@ -35,7 +35,7 @@ const forSidecarJob = (
|
||||
asset: {
|
||||
id?: string;
|
||||
originalPath?: string;
|
||||
files?: { id: string; type: AssetFileType; path: string; isEdited: boolean }[];
|
||||
files?: { id: string; type: AssetFileType; path: string }[];
|
||||
} = {},
|
||||
) => {
|
||||
return {
|
||||
@@ -1084,7 +1084,6 @@ describe(MetadataService.name, () => {
|
||||
id: 'some-id',
|
||||
type: AssetFileType.Sidecar,
|
||||
path: '/path/to/something',
|
||||
isEdited: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1692,7 +1691,7 @@ describe(MetadataService.name, () => {
|
||||
it('should unset sidecar path if file no longer exist', async () => {
|
||||
const asset = forSidecarJob({
|
||||
originalPath: '/path/to/IMG_123.jpg',
|
||||
files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar, isEdited: false }],
|
||||
files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar }],
|
||||
});
|
||||
mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset);
|
||||
mocks.storage.checkFileExists.mockResolvedValue(false);
|
||||
@@ -1705,7 +1704,7 @@ describe(MetadataService.name, () => {
|
||||
it('should do nothing if the sidecar file still exists', async () => {
|
||||
const asset = forSidecarJob({
|
||||
originalPath: '/path/to/IMG_123.jpg',
|
||||
files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar, isEdited: false }],
|
||||
files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar }],
|
||||
});
|
||||
|
||||
mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset);
|
||||
|
||||
@@ -372,7 +372,7 @@ describe(NotificationService.name, () => {
|
||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([
|
||||
{ id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg', isEdited: false },
|
||||
{ id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg' },
|
||||
]);
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
|
||||
@@ -403,7 +403,7 @@ describe(NotificationService.name, () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([{ ...assetStub.image.files[2], isEdited: false }]);
|
||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetStub.image.files[2]]);
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
|
||||
expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith(
|
||||
|
||||
@@ -240,11 +240,11 @@ export class StorageTemplateService extends BaseService {
|
||||
assetInfo: { sizeInBytes: fileSizeInByte, checksum },
|
||||
});
|
||||
|
||||
const sidecarPath = getAssetFile(asset.files, AssetFileType.Sidecar, { isEdited: false })?.path;
|
||||
const sidecarPath = getAssetFile(asset.files, AssetFileType.Sidecar)?.path;
|
||||
if (sidecarPath) {
|
||||
await this.storageCore.moveFile({
|
||||
entityId: id,
|
||||
pathType: AssetFileType.Sidecar,
|
||||
pathType: AssetPathType.Sidecar,
|
||||
oldPath: sidecarPath,
|
||||
newPath: `${newPath}.xmp`,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { GeneratedImageType, StorageCore } from 'src/cores/storage.core';
|
||||
import { AssetFile, Exif } from 'src/database';
|
||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
@@ -14,19 +14,19 @@ import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { IBulkAsset, ImmichFile, UploadFile, UploadRequest } from 'src/types';
|
||||
import { checkAccess } from 'src/utils/access';
|
||||
|
||||
export const getAssetFile = (files: AssetFile[], type: AssetFileType, { isEdited }: { isEdited: boolean }) => {
|
||||
return files.find((file) => file.type === type && file.isEdited === isEdited);
|
||||
export const getAssetFile = (files: AssetFile[], type: AssetFileType | GeneratedImageType) => {
|
||||
return files.find((file) => file.type === type);
|
||||
};
|
||||
|
||||
export const getAssetFiles = (files: AssetFile[]) => ({
|
||||
fullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: false }),
|
||||
previewFile: getAssetFile(files, AssetFileType.Preview, { isEdited: false }),
|
||||
thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail, { isEdited: false }),
|
||||
sidecarFile: getAssetFile(files, AssetFileType.Sidecar, { isEdited: false }),
|
||||
fullsizeFile: getAssetFile(files, AssetFileType.FullSize),
|
||||
previewFile: getAssetFile(files, AssetFileType.Preview),
|
||||
thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail),
|
||||
sidecarFile: getAssetFile(files, AssetFileType.Sidecar),
|
||||
|
||||
editedFullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: true }),
|
||||
editedPreviewFile: getAssetFile(files, AssetFileType.Preview, { isEdited: true }),
|
||||
editedThumbnailFile: getAssetFile(files, AssetFileType.Preview, { isEdited: true }),
|
||||
editedFullsizeFile: getAssetFile(files, AssetFileType.FullSizeEdited),
|
||||
editedPreviewFile: getAssetFile(files, AssetFileType.PreviewEdited),
|
||||
editedThumbnailFile: getAssetFile(files, AssetFileType.ThumbnailEdited),
|
||||
});
|
||||
|
||||
export const addAssets = async (
|
||||
|
||||
59
server/test/fixtures/asset.stub.ts
vendored
59
server/test/fixtures/asset.stub.ts
vendored
@@ -31,21 +31,18 @@ const sidecarFileWithoutExt = factory.assetFile({
|
||||
});
|
||||
|
||||
const editedPreviewFile = factory.assetFile({
|
||||
type: AssetFileType.Preview,
|
||||
type: AssetFileType.PreviewEdited,
|
||||
path: '/uploads/user-id/preview/path_edited.jpg',
|
||||
isEdited: true,
|
||||
});
|
||||
|
||||
const editedThumbnailFile = factory.assetFile({
|
||||
type: AssetFileType.Thumbnail,
|
||||
type: AssetFileType.ThumbnailEdited,
|
||||
path: '/uploads/user-id/thumbnail/path_edited.jpg',
|
||||
isEdited: true,
|
||||
});
|
||||
|
||||
const editedFullsizeFile = factory.assetFile({
|
||||
type: AssetFileType.FullSize,
|
||||
type: AssetFileType.FullSizeEdited,
|
||||
path: '/uploads/user-id/fullsize/path_edited.jpg',
|
||||
isEdited: true,
|
||||
});
|
||||
|
||||
const files: AssetFile[] = [fullsizeFile, previewFile, thumbnailFile];
|
||||
@@ -89,7 +86,7 @@ export const assetStub = {
|
||||
make: 'FUJIFILM',
|
||||
model: 'X-T50',
|
||||
lensModel: 'XF27mm F2.8 R WR',
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
...asset,
|
||||
}),
|
||||
noResizePath: Object.freeze({
|
||||
@@ -129,7 +126,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
noWebpPath: Object.freeze({
|
||||
@@ -171,7 +168,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
noThumbhash: Object.freeze({
|
||||
@@ -210,7 +207,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
primaryImage: Object.freeze({
|
||||
@@ -259,7 +256,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
image: Object.freeze({
|
||||
@@ -306,7 +303,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
trashed: Object.freeze({
|
||||
@@ -350,7 +347,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
trashedOffline: Object.freeze({
|
||||
@@ -394,7 +391,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
archived: Object.freeze({
|
||||
id: 'asset-id',
|
||||
@@ -437,7 +434,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
external: Object.freeze({
|
||||
@@ -480,7 +477,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
image1: Object.freeze({
|
||||
@@ -523,7 +520,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
imageFrom2015: Object.freeze({
|
||||
@@ -565,7 +562,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
video: Object.freeze({
|
||||
@@ -609,7 +606,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
livePhotoMotionAsset: Object.freeze({
|
||||
@@ -630,7 +627,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [] as AssetEditActionItem[],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif; edits: AssetEditActionItem[] }),
|
||||
|
||||
livePhotoStillAsset: Object.freeze({
|
||||
@@ -652,7 +649,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [] as AssetEditActionItem[],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }),
|
||||
|
||||
livePhotoWithOriginalFileName: Object.freeze({
|
||||
@@ -676,7 +673,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [] as AssetEditActionItem[],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }),
|
||||
|
||||
withLocation: Object.freeze({
|
||||
@@ -724,7 +721,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
sidecar: Object.freeze({
|
||||
@@ -763,7 +760,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
sidecarWithoutExt: Object.freeze({
|
||||
@@ -799,7 +796,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
hasEncodedVideo: Object.freeze({
|
||||
@@ -842,7 +839,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
hasFileExtension: Object.freeze({
|
||||
@@ -882,7 +879,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
imageDng: Object.freeze({
|
||||
@@ -926,7 +923,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
imageHif: Object.freeze({
|
||||
@@ -970,7 +967,7 @@ export const assetStub = {
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
panoramaTif: Object.freeze({
|
||||
@@ -1071,7 +1068,7 @@ export const assetStub = {
|
||||
},
|
||||
},
|
||||
] as AssetEditActionItem[],
|
||||
editCount: 1,
|
||||
isEdited: true,
|
||||
}),
|
||||
|
||||
withoutEdits: Object.freeze({
|
||||
@@ -1119,6 +1116,6 @@ export const assetStub = {
|
||||
width: 2160,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
edits: [],
|
||||
editCount: 0,
|
||||
isEdited: false,
|
||||
}),
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user