mirror of
https://github.com/immich-app/immich.git
synced 2025-12-24 01:11:32 +03:00
feat(server): read-write external assets (#9235)
* refactor: remove isReadOnly and isExternal usages * chore: open api * fix: linting * remove mobile isReadOnly dependency --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -32,7 +32,6 @@ class Asset {
|
||||
isFavorite = remote.isFavorite,
|
||||
isArchived = remote.isArchived,
|
||||
isTrashed = remote.isTrashed,
|
||||
isReadOnly = remote.isReadOnly,
|
||||
isOffline = remote.isOffline,
|
||||
// workaround to nullify stackParentId for the parent asset until we refactor the mobile app
|
||||
// stack handling to properly handle it
|
||||
@@ -55,7 +54,6 @@ class Asset {
|
||||
isFavorite = local.isFavorite,
|
||||
isArchived = false,
|
||||
isTrashed = false,
|
||||
isReadOnly = false,
|
||||
isOffline = false,
|
||||
stackCount = 0,
|
||||
fileCreatedAt = local.createDateTime {
|
||||
@@ -90,7 +88,6 @@ class Asset {
|
||||
this.isTrashed = false,
|
||||
this.stackParentId,
|
||||
this.stackCount = 0,
|
||||
this.isReadOnly = false,
|
||||
this.isOffline = false,
|
||||
this.thumbhash,
|
||||
});
|
||||
@@ -161,8 +158,6 @@ class Asset {
|
||||
|
||||
bool isTrashed;
|
||||
|
||||
bool isReadOnly;
|
||||
|
||||
bool isOffline;
|
||||
|
||||
@ignore
|
||||
@@ -278,7 +273,6 @@ class Asset {
|
||||
isFavorite != a.isFavorite ||
|
||||
isArchived != a.isArchived ||
|
||||
isTrashed != a.isTrashed ||
|
||||
isReadOnly != a.isReadOnly ||
|
||||
isOffline != a.isOffline ||
|
||||
a.exifInfo?.latitude != exifInfo?.latitude ||
|
||||
a.exifInfo?.longitude != exifInfo?.longitude ||
|
||||
@@ -324,7 +318,6 @@ class Asset {
|
||||
isFavorite: isFavorite,
|
||||
isArchived: isArchived,
|
||||
isTrashed: isTrashed,
|
||||
isReadOnly: isReadOnly,
|
||||
isOffline: isOffline,
|
||||
);
|
||||
}
|
||||
@@ -345,7 +338,6 @@ class Asset {
|
||||
isFavorite: a.isFavorite,
|
||||
isArchived: a.isArchived,
|
||||
isTrashed: a.isTrashed,
|
||||
isReadOnly: a.isReadOnly,
|
||||
isOffline: a.isOffline,
|
||||
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
|
||||
thumbhash: a.thumbhash,
|
||||
@@ -380,7 +372,6 @@ class Asset {
|
||||
bool? isFavorite,
|
||||
bool? isArchived,
|
||||
bool? isTrashed,
|
||||
bool? isReadOnly,
|
||||
bool? isOffline,
|
||||
ExifInfo? exifInfo,
|
||||
String? stackParentId,
|
||||
@@ -405,7 +396,6 @@ class Asset {
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
isArchived: isArchived ?? this.isArchived,
|
||||
isTrashed: isTrashed ?? this.isTrashed,
|
||||
isReadOnly: isReadOnly ?? this.isReadOnly,
|
||||
isOffline: isOffline ?? this.isOffline,
|
||||
exifInfo: exifInfo ?? this.exifInfo,
|
||||
stackParentId: stackParentId ?? this.stackParentId,
|
||||
@@ -470,7 +460,6 @@ class Asset {
|
||||
"height": ${height ?? "N/A"},
|
||||
"isArchived": $isArchived,
|
||||
"isTrashed": $isTrashed,
|
||||
"isReadOnly": $isReadOnly,
|
||||
"isOffline": $isOffline,
|
||||
}""";
|
||||
}
|
||||
|
||||
135
mobile/lib/entities/asset.entity.g.dart
generated
135
mobile/lib/entities/asset.entity.g.dart
generated
@@ -62,64 +62,59 @@ const AssetSchema = CollectionSchema(
|
||||
name: r'isOffline',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'isReadOnly': PropertySchema(
|
||||
id: 9,
|
||||
name: r'isReadOnly',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'isTrashed': PropertySchema(
|
||||
id: 10,
|
||||
id: 9,
|
||||
name: r'isTrashed',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'livePhotoVideoId': PropertySchema(
|
||||
id: 11,
|
||||
id: 10,
|
||||
name: r'livePhotoVideoId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'localId': PropertySchema(
|
||||
id: 12,
|
||||
id: 11,
|
||||
name: r'localId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'ownerId': PropertySchema(
|
||||
id: 13,
|
||||
id: 12,
|
||||
name: r'ownerId',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'remoteId': PropertySchema(
|
||||
id: 14,
|
||||
id: 13,
|
||||
name: r'remoteId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'stackCount': PropertySchema(
|
||||
id: 15,
|
||||
id: 14,
|
||||
name: r'stackCount',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'stackParentId': PropertySchema(
|
||||
id: 16,
|
||||
id: 15,
|
||||
name: r'stackParentId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'thumbhash': PropertySchema(
|
||||
id: 17,
|
||||
id: 16,
|
||||
name: r'thumbhash',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'type': PropertySchema(
|
||||
id: 18,
|
||||
id: 17,
|
||||
name: r'type',
|
||||
type: IsarType.byte,
|
||||
enumMap: _AssettypeEnumValueMap,
|
||||
),
|
||||
r'updatedAt': PropertySchema(
|
||||
id: 19,
|
||||
id: 18,
|
||||
name: r'updatedAt',
|
||||
type: IsarType.dateTime,
|
||||
),
|
||||
r'width': PropertySchema(
|
||||
id: 20,
|
||||
id: 19,
|
||||
name: r'width',
|
||||
type: IsarType.int,
|
||||
)
|
||||
@@ -239,18 +234,17 @@ void _assetSerialize(
|
||||
writer.writeBool(offsets[6], object.isArchived);
|
||||
writer.writeBool(offsets[7], object.isFavorite);
|
||||
writer.writeBool(offsets[8], object.isOffline);
|
||||
writer.writeBool(offsets[9], object.isReadOnly);
|
||||
writer.writeBool(offsets[10], object.isTrashed);
|
||||
writer.writeString(offsets[11], object.livePhotoVideoId);
|
||||
writer.writeString(offsets[12], object.localId);
|
||||
writer.writeLong(offsets[13], object.ownerId);
|
||||
writer.writeString(offsets[14], object.remoteId);
|
||||
writer.writeLong(offsets[15], object.stackCount);
|
||||
writer.writeString(offsets[16], object.stackParentId);
|
||||
writer.writeString(offsets[17], object.thumbhash);
|
||||
writer.writeByte(offsets[18], object.type.index);
|
||||
writer.writeDateTime(offsets[19], object.updatedAt);
|
||||
writer.writeInt(offsets[20], object.width);
|
||||
writer.writeBool(offsets[9], object.isTrashed);
|
||||
writer.writeString(offsets[10], object.livePhotoVideoId);
|
||||
writer.writeString(offsets[11], object.localId);
|
||||
writer.writeLong(offsets[12], object.ownerId);
|
||||
writer.writeString(offsets[13], object.remoteId);
|
||||
writer.writeLong(offsets[14], object.stackCount);
|
||||
writer.writeString(offsets[15], object.stackParentId);
|
||||
writer.writeString(offsets[16], object.thumbhash);
|
||||
writer.writeByte(offsets[17], object.type.index);
|
||||
writer.writeDateTime(offsets[18], object.updatedAt);
|
||||
writer.writeInt(offsets[19], object.width);
|
||||
}
|
||||
|
||||
Asset _assetDeserialize(
|
||||
@@ -270,19 +264,18 @@ Asset _assetDeserialize(
|
||||
isArchived: reader.readBoolOrNull(offsets[6]) ?? false,
|
||||
isFavorite: reader.readBoolOrNull(offsets[7]) ?? false,
|
||||
isOffline: reader.readBoolOrNull(offsets[8]) ?? false,
|
||||
isReadOnly: reader.readBoolOrNull(offsets[9]) ?? false,
|
||||
isTrashed: reader.readBoolOrNull(offsets[10]) ?? false,
|
||||
livePhotoVideoId: reader.readStringOrNull(offsets[11]),
|
||||
localId: reader.readStringOrNull(offsets[12]),
|
||||
ownerId: reader.readLong(offsets[13]),
|
||||
remoteId: reader.readStringOrNull(offsets[14]),
|
||||
stackCount: reader.readLongOrNull(offsets[15]),
|
||||
stackParentId: reader.readStringOrNull(offsets[16]),
|
||||
thumbhash: reader.readStringOrNull(offsets[17]),
|
||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ??
|
||||
isTrashed: reader.readBoolOrNull(offsets[9]) ?? false,
|
||||
livePhotoVideoId: reader.readStringOrNull(offsets[10]),
|
||||
localId: reader.readStringOrNull(offsets[11]),
|
||||
ownerId: reader.readLong(offsets[12]),
|
||||
remoteId: reader.readStringOrNull(offsets[13]),
|
||||
stackCount: reader.readLongOrNull(offsets[14]),
|
||||
stackParentId: reader.readStringOrNull(offsets[15]),
|
||||
thumbhash: reader.readStringOrNull(offsets[16]),
|
||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ??
|
||||
AssetType.other,
|
||||
updatedAt: reader.readDateTime(offsets[19]),
|
||||
width: reader.readIntOrNull(offsets[20]),
|
||||
updatedAt: reader.readDateTime(offsets[18]),
|
||||
width: reader.readIntOrNull(offsets[19]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
@@ -315,27 +308,25 @@ P _assetDeserializeProp<P>(
|
||||
case 9:
|
||||
return (reader.readBoolOrNull(offset) ?? false) as P;
|
||||
case 10:
|
||||
return (reader.readBoolOrNull(offset) ?? false) as P;
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 11:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 12:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 13:
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 14:
|
||||
case 13:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 15:
|
||||
case 14:
|
||||
return (reader.readLongOrNull(offset)) as P;
|
||||
case 15:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 16:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 17:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 18:
|
||||
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||
AssetType.other) as P;
|
||||
case 19:
|
||||
case 18:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
case 20:
|
||||
case 19:
|
||||
return (reader.readIntOrNull(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
@@ -1366,16 +1357,6 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> isReadOnlyEqualTo(
|
||||
bool value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'isReadOnly',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> isTrashedEqualTo(
|
||||
bool value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -2527,18 +2508,6 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsReadOnly() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isReadOnly', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsReadOnlyDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isReadOnly', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashed() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isTrashed', Sort.asc);
|
||||
@@ -2793,18 +2762,6 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsReadOnly() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isReadOnly', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsReadOnlyDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isReadOnly', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashed() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isTrashed', Sort.asc);
|
||||
@@ -2995,12 +2952,6 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByIsReadOnly() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'isReadOnly');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByIsTrashed() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'isTrashed');
|
||||
@@ -3136,12 +3087,6 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, bool, QQueryOperations> isReadOnlyProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'isReadOnly');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, bool, QQueryOperations> isTrashedProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'isTrashed');
|
||||
|
||||
@@ -71,19 +71,6 @@ extension AssetListExtension on Iterable<Asset> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Returns the assets that are present on a file system which has write permission
|
||||
/// This filters out assets on readOnly external library to which we cannot perform any write operation
|
||||
Iterable<Asset> writableOnly({
|
||||
void Function()? errorCallback,
|
||||
}) {
|
||||
final bool onlyWritable = every((e) => !e.isReadOnly);
|
||||
if (!onlyWritable) {
|
||||
if (errorCallback != null) errorCallback();
|
||||
return where((a) => !a.isReadOnly);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Filters out offline assets and returns those that are still accessible by the Immich server
|
||||
Iterable<Asset> nonOfflineOnly({
|
||||
void Function()? errorCallback,
|
||||
|
||||
@@ -102,16 +102,6 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
}
|
||||
|
||||
void handleDelete() async {
|
||||
// Cannot delete readOnly / external assets. They are handled through library offline jobs
|
||||
if (asset.isReadOnly) {
|
||||
ImmichToast.show(
|
||||
durationInSecond: 1,
|
||||
context: context,
|
||||
msg: 'asset_action_delete_err_read_only'.tr(),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
Future<bool> onDelete(bool force) async {
|
||||
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(
|
||||
{asset},
|
||||
|
||||
@@ -42,7 +42,7 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (asset.isRemote && !asset.isReadOnly)
|
||||
if (asset.isRemote)
|
||||
IconButton(
|
||||
onPressed: () => handleEditDateTime(
|
||||
ref,
|
||||
|
||||
@@ -24,7 +24,7 @@ class ExifLocation extends StatelessWidget {
|
||||
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
|
||||
// Guard no lat/lng
|
||||
if (!hasCoordinates) {
|
||||
return asset.isRemote && !asset.isReadOnly
|
||||
return asset.isRemote
|
||||
? ListTile(
|
||||
minLeadingWidth: 0,
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
@@ -57,7 +57,7 @@ class ExifLocation extends StatelessWidget {
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
).tr(),
|
||||
if (asset.isRemote && !asset.isReadOnly)
|
||||
if (asset.isRemote)
|
||||
IconButton(
|
||||
onPressed: editLocation,
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
|
||||
@@ -63,6 +63,12 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
child: const AllPeoplePage(),
|
||||
);
|
||||
},
|
||||
AllPlacesRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const AllPlacesPage(),
|
||||
);
|
||||
},
|
||||
AllVideosRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
@@ -138,12 +144,6 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
),
|
||||
);
|
||||
},
|
||||
AllPlacesRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const AllPlacesPage(),
|
||||
);
|
||||
},
|
||||
FailedBackupStatusRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
@@ -525,6 +525,20 @@ class AllPeopleRoute extends PageRouteInfo<void> {
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AllPlacesPage]
|
||||
class AllPlacesRoute extends PageRouteInfo<void> {
|
||||
const AllPlacesRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
AllPlacesRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AllPlacesRoute';
|
||||
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AllVideosPage]
|
||||
class AllVideosRoute extends PageRouteInfo<void> {
|
||||
@@ -752,20 +766,6 @@ class CreateAlbumRouteArgs {
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AllPlacesPage]
|
||||
class AllPlacesRoute extends PageRouteInfo<void> {
|
||||
const AllPlacesRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
AllPlacesRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'CuratedLocationRoute';
|
||||
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [FailedBackupStatusPage]
|
||||
class FailedBackupStatusRoute extends PageRouteInfo<void> {
|
||||
|
||||
@@ -184,11 +184,6 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
currentUser,
|
||||
errorCallback: errorBuilder('home_page_delete_err_partner'.tr()),
|
||||
)
|
||||
// Cannot delete readOnly / external assets. They are handled through library offline jobs
|
||||
.writableOnly(
|
||||
errorCallback:
|
||||
errorBuilder('asset_action_delete_err_read_only'.tr()),
|
||||
)
|
||||
.toList();
|
||||
final isDeleted = await ref
|
||||
.read(assetProvider.notifier)
|
||||
@@ -238,13 +233,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
final toDelete = ownedRemoteSelection(
|
||||
localErrorMessage: 'home_page_delete_remote_err_local'.tr(),
|
||||
ownerErrorMessage: 'home_page_delete_err_partner'.tr(),
|
||||
)
|
||||
// Cannot delete readOnly / external assets. They are handled through library offline jobs
|
||||
.writableOnly(
|
||||
errorCallback:
|
||||
errorBuilder('asset_action_delete_err_read_only'.tr()),
|
||||
)
|
||||
.toList();
|
||||
).toList();
|
||||
|
||||
final isDeleted = await ref
|
||||
.read(assetProvider.notifier)
|
||||
@@ -372,12 +361,8 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
final remoteAssets = ownedRemoteSelection(
|
||||
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
||||
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
|
||||
).writableOnly(
|
||||
// Assume readOnly assets to be present in a read-only mount. So do not write sidecar
|
||||
errorCallback: errorBuilder(
|
||||
'multiselect_grid_edit_date_time_err_read_only'.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
if (remoteAssets.isNotEmpty) {
|
||||
handleEditDateTime(ref, context, remoteAssets.toList());
|
||||
}
|
||||
@@ -391,12 +376,8 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
final remoteAssets = ownedRemoteSelection(
|
||||
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
||||
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
|
||||
).writableOnly(
|
||||
// Assume readOnly assets to be present in a read-only mount. So do not write sidecar
|
||||
errorCallback: errorBuilder(
|
||||
'multiselect_grid_edit_gps_err_read_only'.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
if (remoteAssets.isNotEmpty) {
|
||||
handleEditLocation(ref, context, remoteAssets.toList());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user