Compare commits

..

6 Commits

Author SHA1 Message Date
bwees
91d6fedbf2 fix: restore ancestors album to currentRemoteAlbumProvider when popping 2025-09-21 23:19:12 -05:00
bwees
8465d6c493 fix: rebase conflict 2025-09-20 10:31:37 -05:00
bwees
dc8c705420 fix: bottom sheet now is usable when navigating to another asset viewer 2025-09-20 10:31:37 -05:00
bwees
a437a947c3 feat: show "appears in" albums on asset viewer bottom sheet
fix: multiple RemoteAlbumPages in navigation stack

this also allows us to not have to set the current album before navigating to RemoteAlbumPage

chore: clarification comments

handle nested album pages

fix: hide "appears in" when an asset is not in any albums

fix: way more bottom padding

for some reason we can't query the safe area here :/
2025-09-20 10:31:35 -05:00
Alex
aaeac2ab73 fix(web): revert do not upscale small pictures (#22191) (#22233) 2025-09-20 09:25:27 -05:00
Jason Rasmussen
de57fecb69 fix(web): copy to clipboard on safari (#22217) 2025-09-19 17:44:18 -04:00
29 changed files with 196 additions and 7861 deletions

View File

@@ -920,6 +920,7 @@
"cant_get_number_of_comments": "Can't get number of comments",
"cant_search_people": "Can't search people",
"cant_search_places": "Can't search places",
"clipboard_unsupported_mime_type": "The system clipboard does not support copying this type of content: {mimeType}",
"error_adding_assets_to_album": "Error adding assets to album",
"error_adding_users_to_album": "Error adding users to album",
"error_deleting_shared_user": "Error deleting shared user",

View File

@@ -89,8 +89,7 @@ data class PlatformAsset (
val height: Long? = null,
val durationInSeconds: Long,
val orientation: Long,
val isFavorite: Boolean,
val adjustmentTimestamp: Long? = null
val isFavorite: Boolean
)
{
companion object {
@@ -105,8 +104,7 @@ data class PlatformAsset (
val durationInSeconds = pigeonVar_list[7] as Long
val orientation = pigeonVar_list[8] as Long
val isFavorite = pigeonVar_list[9] as Boolean
val adjustmentTimestamp = pigeonVar_list[10] as Long?
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, adjustmentTimestamp)
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite)
}
}
fun toList(): List<Any?> {
@@ -121,7 +119,6 @@ data class PlatformAsset (
durationInSeconds,
orientation,
isFavorite,
adjustmentTimestamp,
)
}
override fun equals(other: Any?): Boolean {

View File

@@ -138,7 +138,6 @@ open class NativeSyncApiImplBase(context: Context) {
duration,
orientation.toLong(),
isFavorite,
adjustmentTimestamp = null
)
yield(AssetResult.ValidAsset(asset, bucketId))
}

File diff suppressed because one or more lines are too long

View File

@@ -140,7 +140,6 @@ struct PlatformAsset: Hashable {
var durationInSeconds: Int64
var orientation: Int64
var isFavorite: Bool
var adjustmentTimestamp: Int64? = nil
// swift-format-ignore: AlwaysUseLowerCamelCase
@@ -155,7 +154,6 @@ struct PlatformAsset: Hashable {
let durationInSeconds = pigeonVar_list[7] as! Int64
let orientation = pigeonVar_list[8] as! Int64
let isFavorite = pigeonVar_list[9] as! Bool
let adjustmentTimestamp: Int64? = nilOrValue(pigeonVar_list[10])
return PlatformAsset(
id: id,
@@ -167,8 +165,7 @@ struct PlatformAsset: Hashable {
height: height,
durationInSeconds: durationInSeconds,
orientation: orientation,
isFavorite: isFavorite,
adjustmentTimestamp: adjustmentTimestamp
isFavorite: isFavorite
)
}
func toList() -> [Any?] {
@@ -183,7 +180,6 @@ struct PlatformAsset: Hashable {
durationInSeconds,
orientation,
isFavorite,
adjustmentTimestamp,
]
}
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {

View File

@@ -12,8 +12,7 @@ extension PHAsset {
height: Int64(pixelHeight),
durationInSeconds: Int64(duration),
orientation: 0,
isFavorite: isFavorite,
adjustmentTimestamp: adjustmentTimestamp
isFavorite: isFavorite
)
}
@@ -24,10 +23,6 @@ extension PHAsset {
var filename: String? {
return value(forKey: "filename") as? String
}
var adjustmentTimestamp: Int64 {
return (value(forKey: "adjustmentTimestamp") as? Date?).map( {Int64($0?.timeIntervalSince1970 ?? 0)} ) ?? 0
}
// This method is expected to be slow as it goes through the asset resources to fetch the originalFilename
var originalFilename: String? {

View File

@@ -4,7 +4,6 @@ class LocalAsset extends BaseAsset {
final String id;
final String? remoteId;
final int orientation;
final int? adjustmentTimestamp;
const LocalAsset({
required this.id,
@@ -20,7 +19,6 @@ class LocalAsset extends BaseAsset {
super.isFavorite = false,
super.livePhotoVideoId,
this.orientation = 0,
this.adjustmentTimestamp,
});
@override
@@ -43,7 +41,6 @@ class LocalAsset extends BaseAsset {
remoteId: ${remoteId ?? "<NA>"}
isFavorite: $isFavorite,
orientation: $orientation,
adjustmentTimestamp: ${adjustmentTimestamp ?? "<NA>"}
}''';
}
@@ -52,15 +49,11 @@ class LocalAsset extends BaseAsset {
bool operator ==(Object other) {
if (other is! LocalAsset) return false;
if (identical(this, other)) return true;
return super == other &&
id == other.id &&
orientation == other.orientation &&
adjustmentTimestamp == other.adjustmentTimestamp;
return super == other && id == other.id && orientation == other.orientation;
}
@override
int get hashCode =>
super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode ^ adjustmentTimestamp.hashCode;
int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode;
LocalAsset copyWith({
String? id,
@@ -75,7 +68,6 @@ class LocalAsset extends BaseAsset {
int? durationInSeconds,
bool? isFavorite,
int? orientation,
int? adjustmentTimestamp,
}) {
return LocalAsset(
id: id ?? this.id,
@@ -90,7 +82,6 @@ class LocalAsset extends BaseAsset {
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
adjustmentTimestamp: adjustmentTimestamp ?? this.adjustmentTimestamp,
);
}
}

View File

@@ -303,7 +303,6 @@ extension on Iterable<PlatformAsset> {
durationInSeconds: e.durationInSeconds,
orientation: e.orientation,
isFavorite: e.isFavorite,
adjustmentTimestamp: e.adjustmentTimestamp,
),
).toList();
}

View File

@@ -160,6 +160,10 @@ class RemoteAlbumService {
return _repository.getCount();
}
Future<List<RemoteAlbum>> getAlbumsContainingAsset(String assetId) {
return _repository.getAlbumsContainingAsset(assetId);
}
Future<List<RemoteAlbum>> _sortByNewestAsset(List<RemoteAlbum> albums) async {
// map album IDs to their newest asset dates
final Map<String, Future<DateTime?>> assetTimestampFutures = {};

View File

@@ -16,8 +16,6 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
IntColumn get orientation => integer().withDefault(const Constant(0))();
IntColumn get adjustmentTimestamp => integer().nullable()();
@override
Set<Column> get primaryKey => {id};
}
@@ -36,6 +34,5 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
width: width,
remoteId: null,
orientation: orientation,
adjustmentTimestamp: adjustmentTimestamp,
);
}

View File

@@ -21,7 +21,6 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder =
i0.Value<String?> checksum,
i0.Value<bool> isFavorite,
i0.Value<int> orientation,
i0.Value<int?> adjustmentTimestamp,
});
typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
i1.LocalAssetEntityCompanion Function({
@@ -36,7 +35,6 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
i0.Value<String?> checksum,
i0.Value<bool> isFavorite,
i0.Value<int> orientation,
i0.Value<int?> adjustmentTimestamp,
});
class $$LocalAssetEntityTableFilterComposer
@@ -103,11 +101,6 @@ class $$LocalAssetEntityTableFilterComposer
column: $table.orientation,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<int> get adjustmentTimestamp => $composableBuilder(
column: $table.adjustmentTimestamp,
builder: (column) => i0.ColumnFilters(column),
);
}
class $$LocalAssetEntityTableOrderingComposer
@@ -173,11 +166,6 @@ class $$LocalAssetEntityTableOrderingComposer
column: $table.orientation,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get adjustmentTimestamp => $composableBuilder(
column: $table.adjustmentTimestamp,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $$LocalAssetEntityTableAnnotationComposer
@@ -227,11 +215,6 @@ class $$LocalAssetEntityTableAnnotationComposer
column: $table.orientation,
builder: (column) => column,
);
i0.GeneratedColumn<int> get adjustmentTimestamp => $composableBuilder(
column: $table.adjustmentTimestamp,
builder: (column) => column,
);
}
class $$LocalAssetEntityTableTableManager
@@ -285,7 +268,6 @@ class $$LocalAssetEntityTableTableManager
i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(),
i0.Value<int> orientation = const i0.Value.absent(),
i0.Value<int?> adjustmentTimestamp = const i0.Value.absent(),
}) => i1.LocalAssetEntityCompanion(
name: name,
type: type,
@@ -298,7 +280,6 @@ class $$LocalAssetEntityTableTableManager
checksum: checksum,
isFavorite: isFavorite,
orientation: orientation,
adjustmentTimestamp: adjustmentTimestamp,
),
createCompanionCallback:
({
@@ -313,7 +294,6 @@ class $$LocalAssetEntityTableTableManager
i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(),
i0.Value<int> orientation = const i0.Value.absent(),
i0.Value<int?> adjustmentTimestamp = const i0.Value.absent(),
}) => i1.LocalAssetEntityCompanion.insert(
name: name,
type: type,
@@ -326,7 +306,6 @@ class $$LocalAssetEntityTableTableManager
checksum: checksum,
isFavorite: isFavorite,
orientation: orientation,
adjustmentTimestamp: adjustmentTimestamp,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
@@ -494,17 +473,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
requiredDuringInsert: false,
defaultValue: const i4.Constant(0),
);
static const i0.VerificationMeta _adjustmentTimestampMeta =
const i0.VerificationMeta('adjustmentTimestamp');
@override
late final i0.GeneratedColumn<int> adjustmentTimestamp =
i0.GeneratedColumn<int>(
'adjustment_timestamp',
aliasedName,
true,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
);
@override
List<i0.GeneratedColumn> get $columns => [
name,
@@ -518,7 +486,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
checksum,
isFavorite,
orientation,
adjustmentTimestamp,
];
@override
String get aliasedName => _alias ?? actualTableName;
@@ -599,15 +566,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
),
);
}
if (data.containsKey('adjustment_timestamp')) {
context.handle(
_adjustmentTimestampMeta,
adjustmentTimestamp.isAcceptableOrUnknown(
data['adjustment_timestamp']!,
_adjustmentTimestampMeta,
),
);
}
return context;
}
@@ -666,10 +624,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
i0.DriftSqlType.int,
data['${effectivePrefix}orientation'],
)!,
adjustmentTimestamp: attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}adjustment_timestamp'],
),
);
}
@@ -699,7 +653,6 @@ class LocalAssetEntityData extends i0.DataClass
final String? checksum;
final bool isFavorite;
final int orientation;
final int? adjustmentTimestamp;
const LocalAssetEntityData({
required this.name,
required this.type,
@@ -712,7 +665,6 @@ class LocalAssetEntityData extends i0.DataClass
this.checksum,
required this.isFavorite,
required this.orientation,
this.adjustmentTimestamp,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
@@ -740,9 +692,6 @@ class LocalAssetEntityData extends i0.DataClass
}
map['is_favorite'] = i0.Variable<bool>(isFavorite);
map['orientation'] = i0.Variable<int>(orientation);
if (!nullToAbsent || adjustmentTimestamp != null) {
map['adjustment_timestamp'] = i0.Variable<int>(adjustmentTimestamp);
}
return map;
}
@@ -765,9 +714,6 @@ class LocalAssetEntityData extends i0.DataClass
checksum: serializer.fromJson<String?>(json['checksum']),
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
orientation: serializer.fromJson<int>(json['orientation']),
adjustmentTimestamp: serializer.fromJson<int?>(
json['adjustmentTimestamp'],
),
);
}
@override
@@ -787,7 +733,6 @@ class LocalAssetEntityData extends i0.DataClass
'checksum': serializer.toJson<String?>(checksum),
'isFavorite': serializer.toJson<bool>(isFavorite),
'orientation': serializer.toJson<int>(orientation),
'adjustmentTimestamp': serializer.toJson<int?>(adjustmentTimestamp),
};
}
@@ -803,7 +748,6 @@ class LocalAssetEntityData extends i0.DataClass
i0.Value<String?> checksum = const i0.Value.absent(),
bool? isFavorite,
int? orientation,
i0.Value<int?> adjustmentTimestamp = const i0.Value.absent(),
}) => i1.LocalAssetEntityData(
name: name ?? this.name,
type: type ?? this.type,
@@ -818,9 +762,6 @@ class LocalAssetEntityData extends i0.DataClass
checksum: checksum.present ? checksum.value : this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
adjustmentTimestamp: adjustmentTimestamp.present
? adjustmentTimestamp.value
: this.adjustmentTimestamp,
);
LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) {
return LocalAssetEntityData(
@@ -841,9 +782,6 @@ class LocalAssetEntityData extends i0.DataClass
orientation: data.orientation.present
? data.orientation.value
: this.orientation,
adjustmentTimestamp: data.adjustmentTimestamp.present
? data.adjustmentTimestamp.value
: this.adjustmentTimestamp,
);
}
@@ -860,8 +798,7 @@ class LocalAssetEntityData extends i0.DataClass
..write('id: $id, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ')
..write('orientation: $orientation, ')
..write('adjustmentTimestamp: $adjustmentTimestamp')
..write('orientation: $orientation')
..write(')'))
.toString();
}
@@ -879,7 +816,6 @@ class LocalAssetEntityData extends i0.DataClass
checksum,
isFavorite,
orientation,
adjustmentTimestamp,
);
@override
bool operator ==(Object other) =>
@@ -895,8 +831,7 @@ class LocalAssetEntityData extends i0.DataClass
other.id == this.id &&
other.checksum == this.checksum &&
other.isFavorite == this.isFavorite &&
other.orientation == this.orientation &&
other.adjustmentTimestamp == this.adjustmentTimestamp);
other.orientation == this.orientation);
}
class LocalAssetEntityCompanion
@@ -912,7 +847,6 @@ class LocalAssetEntityCompanion
final i0.Value<String?> checksum;
final i0.Value<bool> isFavorite;
final i0.Value<int> orientation;
final i0.Value<int?> adjustmentTimestamp;
const LocalAssetEntityCompanion({
this.name = const i0.Value.absent(),
this.type = const i0.Value.absent(),
@@ -925,7 +859,6 @@ class LocalAssetEntityCompanion
this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(),
this.orientation = const i0.Value.absent(),
this.adjustmentTimestamp = const i0.Value.absent(),
});
LocalAssetEntityCompanion.insert({
required String name,
@@ -939,7 +872,6 @@ class LocalAssetEntityCompanion
this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(),
this.orientation = const i0.Value.absent(),
this.adjustmentTimestamp = const i0.Value.absent(),
}) : name = i0.Value(name),
type = i0.Value(type),
id = i0.Value(id);
@@ -955,7 +887,6 @@ class LocalAssetEntityCompanion
i0.Expression<String>? checksum,
i0.Expression<bool>? isFavorite,
i0.Expression<int>? orientation,
i0.Expression<int>? adjustmentTimestamp,
}) {
return i0.RawValuesInsertable({
if (name != null) 'name': name,
@@ -969,8 +900,6 @@ class LocalAssetEntityCompanion
if (checksum != null) 'checksum': checksum,
if (isFavorite != null) 'is_favorite': isFavorite,
if (orientation != null) 'orientation': orientation,
if (adjustmentTimestamp != null)
'adjustment_timestamp': adjustmentTimestamp,
});
}
@@ -986,7 +915,6 @@ class LocalAssetEntityCompanion
i0.Value<String?>? checksum,
i0.Value<bool>? isFavorite,
i0.Value<int>? orientation,
i0.Value<int?>? adjustmentTimestamp,
}) {
return i1.LocalAssetEntityCompanion(
name: name ?? this.name,
@@ -1000,7 +928,6 @@ class LocalAssetEntityCompanion
checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
adjustmentTimestamp: adjustmentTimestamp ?? this.adjustmentTimestamp,
);
}
@@ -1042,9 +969,6 @@ class LocalAssetEntityCompanion
if (orientation.present) {
map['orientation'] = i0.Variable<int>(orientation.value);
}
if (adjustmentTimestamp.present) {
map['adjustment_timestamp'] = i0.Variable<int>(adjustmentTimestamp.value);
}
return map;
}
@@ -1061,8 +985,7 @@ class LocalAssetEntityCompanion
..write('id: $id, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ')
..write('orientation: $orientation, ')
..write('adjustmentTimestamp: $adjustmentTimestamp')
..write('orientation: $orientation')
..write(')'))
.toString();
}

View File

@@ -93,7 +93,7 @@ class Drift extends $Drift implements IDatabaseRepository {
}
@override
int get schemaVersion => 12;
int get schemaVersion => 11;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -159,9 +159,6 @@ class Drift extends $Drift implements IDatabaseRepository {
from10To11: (m, v11) async {
await m.addColumn(v11.localAlbumAssetEntity, v11.localAlbumAssetEntity.marker_);
},
from11To12: (m, v12) async {
await m.addColumn(v12.localAssetEntity, v12.localAssetEntity.adjustmentTimestamp);
},
),
);

View File

@@ -4659,420 +4659,6 @@ class Shape22 extends i0.VersionedTable {
columnsByName['marker']! as i1.GeneratedColumn<bool>;
}
final class Schema12 extends i0.VersionedSchema {
Schema12({required super.database}) : super(version: 12);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAssetChecksum,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
idxLatLng,
];
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 Shape17 remoteAssetEntity = Shape17(
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,
],
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 Shape23 localAssetEntity = Shape23(
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_95,
],
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 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 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,
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
}
class Shape23 extends i0.VersionedTable {
Shape23({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<int> get orientation =>
columnsByName['orientation']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get adjustmentTimestamp =>
columnsByName['adjustment_timestamp']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_95(String aliasedName) =>
i1.GeneratedColumn<int>(
'adjustment_timestamp',
aliasedName,
true,
type: i1.DriftSqlType.int,
);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -5084,7 +4670,6 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -5138,11 +4723,6 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from10To11(migrator, schema);
return 11;
case 11:
final schema = Schema12(database: database);
final migrator = i1.Migrator(database, schema);
await from11To12(migrator, schema);
return 12;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -5160,7 +4740,6 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -5173,6 +4752,5 @@ i1.OnUpgrade stepByStep({
from8To9: from8To9,
from9To10: from9To10,
from10To11: from10To11,
from11To12: from11To12,
),
);

View File

@@ -263,7 +263,6 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
orientation: Value(asset.orientation),
checksum: const Value(null),
isFavorite: Value(asset.isFavorite),
adjustmentTimestamp: Value(asset.adjustmentTimestamp),
);
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
_db.localAssetEntity,

View File

@@ -372,6 +372,18 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
return query.map((row) => row.read(_db.remoteAssetEntity.id)!).get();
}
Future<List<RemoteAlbum>> getAlbumsContainingAsset(String assetId) async {
final albumIdsQuery = _db.remoteAlbumAssetEntity.select()..where((row) => row.assetId.equals(assetId));
final albumIds = (await albumIdsQuery.get()).map((e) => e.albumId).toSet();
if (albumIds.isEmpty) {
return [];
}
return getAll().then((albums) => albums.where((album) => albumIds.contains(album.id)).toList());
}
}
extension on RemoteAlbumEntityData {

View File

@@ -32,6 +32,7 @@ import 'package:immich_mobile/theme/dynamic_theme.dart';
import 'package:immich_mobile/theme/theme_data.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:immich_mobile/utils/licenses.dart';
import 'package:immich_mobile/utils/migration.dart';
@@ -39,7 +40,6 @@ import 'package:intl/date_symbol_data_local.dart';
import 'package:logging/logging.dart';
import 'package:timezone/data/latest.dart';
import 'package:worker_manager/worker_manager.dart';
import 'package:immich_mobile/utils/debug_print.dart';
void main() async {
ImmichWidgetsBinding();

View File

@@ -41,7 +41,6 @@ class PlatformAsset {
required this.durationInSeconds,
required this.orientation,
required this.isFavorite,
this.adjustmentTimestamp,
});
String id;
@@ -64,22 +63,8 @@ class PlatformAsset {
bool isFavorite;
int? adjustmentTimestamp;
List<Object?> _toList() {
return <Object?>[
id,
name,
type,
createdAt,
updatedAt,
width,
height,
durationInSeconds,
orientation,
isFavorite,
adjustmentTimestamp,
];
return <Object?>[id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite];
}
Object encode() {
@@ -99,7 +84,6 @@ class PlatformAsset {
durationInSeconds: result[7]! as int,
orientation: result[8]! as int,
isFavorite: result[9]! as bool,
adjustmentTimestamp: result[10] as int?,
);
}

View File

@@ -129,7 +129,6 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
properties.insert(4, _PropertyItem(label: 'Orientation', value: asset.orientation.toString()));
final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id);
properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', ')));
properties.add(_PropertyItem(label: 'Adjustment Timestamp', value: asset.adjustmentTimestamp?.toString()));
}
Future<void> _addRemoteAssetProperties(RemoteAsset asset) async {

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -221,14 +222,24 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) {
if (didPop) {
Future.microtask(() {
if (mounted) {
ref.read(currentRemoteAlbumProvider.notifier).dispose();
ref.read(remoteAlbumProvider.notifier).refresh();
}
});
if (didPop || !mounted) {
return;
}
final ancestors = context.router.stack.take(context.router.stack.length - 1);
final ancestorPage = ancestors.lastWhereOrNull((route) {
return route.name == RemoteAlbumRoute.page.name;
});
Navigator.of(context).pop();
if (ancestorPage == null) {
ref.read(currentRemoteAlbumProvider.notifier).dispose();
} else {
final album = (ancestorPage.routeData.args as RemoteAlbumRouteArgs).album;
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
}
},
child: ProviderScope(

View File

@@ -12,7 +12,7 @@ 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/models/albums/album_search.model.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
@@ -516,38 +516,6 @@ class _AlbumList extends ConsumerWidget {
sliver: SliverList.builder(
itemBuilder: (_, index) {
final album = albums[index];
final albumTile = LargeLeadingTile(
title: Text(
album.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
),
subtitle: Text(
'${'items_count'.t(context: context, args: {'count': album.assetCount})} • ${album.ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': album.ownerName}) : 'owned'.t(context: context)}',
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
onTap: () => onAlbumSelected(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),
),
),
);
final isOwner = album.ownerId == userId;
if (isOwner) {
@@ -576,11 +544,14 @@ class _AlbumList extends ConsumerWidget {
onDismissed: (direction) async {
await ref.read(remoteAlbumProvider.notifier).deleteAlbum(album.id);
},
child: albumTile,
child: AlbumTile(album: album, isOwner: isOwner, onAlbumSelected: onAlbumSelected),
),
);
} else {
return Padding(padding: const EdgeInsets.only(bottom: 8.0), child: albumTile);
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: AlbumTile(album: album, isOwner: isOwner, onAlbumSelected: onAlbumSelected),
);
}
},
itemCount: albums.length,

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.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';
class AlbumTile extends StatelessWidget {
const AlbumTile({super.key, required this.album, required this.isOwner, this.onAlbumSelected});
final RemoteAlbum album;
final bool isOwner;
final Function(RemoteAlbum)? onAlbumSelected;
@override
Widget build(BuildContext context) {
return LargeLeadingTile(
title: Text(
album.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
),
subtitle: Text(
'${'items_count'.t(context: context, args: {'count': album.assetCount})} • ${isOwner ? 'owned'.t(context: context) : 'shared_by_user'.t(context: context, args: {'user': album.ownerName})}',
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
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),
),
),
);
}
}

View File

@@ -1,3 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -8,17 +10,21 @@ import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/build_context_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';
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/action_button.utils.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -130,6 +136,58 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context);
}
Widget _buildAppearsInList(WidgetRef ref, BuildContext context) {
final isRemote = ref.watch(currentAssetNotifier)?.hasRemote ?? false;
if (!isRemote) {
return const SizedBox.shrink();
}
final remoteAsset = ref.watch(currentAssetNotifier) as RemoteAsset;
final albums = ref.watch(remoteAlbumServiceProvider).getAlbumsContainingAsset(remoteAsset.id);
final userId = ref.watch(currentUserProvider)?.id;
return FutureBuilder(
future: albums,
builder: (_, snap) {
final albums = snap.data ?? [];
if (albums.isEmpty) {
return const SizedBox.shrink();
}
albums.sortBy((a) => a.name);
return Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 8),
child: Column(
spacing: 12,
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,
),
),
...albums.map((album) {
final isOwner = album.ownerId == userId;
return AlbumTile(
album: album,
isOwner: isOwner,
onAlbumSelected: (album) async {
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
ref.invalidate(assetViewerProvider);
context.router.popAndPush(RemoteAlbumRoute(album: album));
},
);
}),
],
),
);
},
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier);
@@ -185,6 +243,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
),
),
// Appears in (Albums)
_buildAppearsInList(ref, context),
// padding at the bottom to avoid cut-off
const SizedBox(height: 100),
],
);
}

View File

@@ -303,7 +303,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftBackupAlbumSelectionRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: LocalTimelineRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: MainTimelineRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: RemoteAlbumRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: RemoteAlbumRoute.page, guards: [_authGuard]),
AutoRoute(
page: AssetViewerRoute.page,
guards: [_authGuard, _duplicateGuard],

View File

@@ -18,7 +18,6 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart';
class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget {
@@ -89,7 +88,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState<RemoteAlbumSliverAppBa
color: actionIconColor,
shadows: actionIconShadows,
),
onPressed: () => context.navigateTo(const TabShellRoute(children: [DriftAlbumsRoute()])),
onPressed: () => context.maybePop(),
),
actions: [
if (widget.onToggleAlbumOrder != null)

View File

@@ -24,7 +24,6 @@ class PlatformAsset {
final int durationInSeconds;
final int orientation;
final bool isFavorite;
final int? adjustmentTimestamp;
const PlatformAsset({
required this.id,
@@ -37,7 +36,6 @@ class PlatformAsset {
this.durationInSeconds = 0,
this.orientation = 0,
this.isFavorite = false,
this.adjustmentTimestamp,
});
}

View File

@@ -14,7 +14,6 @@ import 'schema_v8.dart' as v8;
import 'schema_v9.dart' as v9;
import 'schema_v10.dart' as v10;
import 'schema_v11.dart' as v11;
import 'schema_v12.dart' as v12;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@@ -42,12 +41,10 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v10.DatabaseAtV10(db);
case 11:
return v11.DatabaseAtV11(db);
case 12:
return v12.DatabaseAtV12(db);
default:
throw MissingSchemaException(version, versions);
}
}
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
}

File diff suppressed because it is too large Load Diff

View File

@@ -97,12 +97,15 @@
}
try {
await copyImageToClipboard($photoViewerImgElement ?? assetFileUrl);
notificationController.show({
type: NotificationType.Info,
message: $t('copied_image_to_clipboard'),
timeout: 3000,
});
const result = await copyImageToClipboard($photoViewerImgElement ?? assetFileUrl);
if (result.success) {
notificationController.show({ type: NotificationType.Info, message: $t('copied_image_to_clipboard') });
} else {
notificationController.show({
type: NotificationType.Error,
message: $t('errors.clipboard_unsupported_mime_type', { values: { mimeType: result.mimeType } }),
});
}
} catch (error) {
handleError(error, $t('copy_error'));
}
@@ -240,7 +243,7 @@
use:zoomImageAction
use:swipe={() => ({})}
onswipe={onSwipe}
class="h-full w-full flex"
class="h-full w-full"
transition:fade={{ duration: haveFadeTransition ? assetViewerFadeDuration : 0 }}
>
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
@@ -255,7 +258,7 @@
bind:this={$photoViewerImgElement}
src={assetFileUrl}
alt={$getAltText(toTimelineAsset(asset))}
class="max-h-full max-w-full h-auto w-auto mx-auto my-auto {$slideshowState === SlideshowState.None
class="h-full w-full {$slideshowState === SlideshowState.None
? 'object-contain'
: slideshowLookCssMapping[$slideshowLook]}"
draggable="false"

View File

@@ -625,7 +625,21 @@ const urlToBlob = async (imageSource: string) => {
return await response.blob();
};
export const copyImageToClipboard = async (source: HTMLImageElement | string) => {
const blob = source instanceof HTMLImageElement ? await imgToBlob(source) : await urlToBlob(source);
export const copyImageToClipboard = async (
source: HTMLImageElement | string,
): Promise<{ success: true } | { success: false; mimeType: string }> => {
if (source instanceof HTMLImageElement) {
// do not await, so the Safari clipboard write happens in the context of the user gesture
await navigator.clipboard.write([new ClipboardItem({ ['image/png']: imgToBlob(source) })]);
return { success: true };
}
// if we had a way to get the mime type synchronously, we could do the same thing here
const blob = await urlToBlob(source);
if (!ClipboardItem.supports(blob.type)) {
return { success: false, mimeType: blob.type };
}
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
return { success: true };
};