mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
refactor(mobile): share action button in new timeline (#19967)
* share asset button * include source * move to repository * formatting
This commit is contained in:
parent
531515daf9
commit
055b930066
@ -1693,6 +1693,7 @@
|
|||||||
"settings_saved": "Settings saved",
|
"settings_saved": "Settings saved",
|
||||||
"setup_pin_code": "Setup a PIN code",
|
"setup_pin_code": "Setup a PIN code",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
|
"share_action_prompt": "Shared {count} assets",
|
||||||
"share_add_photos": "Add photos",
|
"share_add_photos": "Add photos",
|
||||||
"share_assets_selected": "{count} selected",
|
"share_assets_selected": "{count} selected",
|
||||||
"share_dialog_preparing": "Preparing...",
|
"share_dialog_preparing": "Preparing...",
|
||||||
|
@ -1,12 +1,49 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class ShareActionButton extends ConsumerWidget {
|
class ShareActionButton extends ConsumerWidget {
|
||||||
const ShareActionButton({super.key});
|
final ActionSource source;
|
||||||
|
|
||||||
|
const ShareActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).shareAssets(source);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: ToastType.error,
|
||||||
|
);
|
||||||
|
} else if (result.count > 0) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'share_action_prompt'
|
||||||
|
.t(context: context, args: {'count': result.count.toString()}),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: ToastType.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@ -14,6 +51,7 @@ class ShareActionButton extends ConsumerWidget {
|
|||||||
iconData:
|
iconData:
|
||||||
Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
|
Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
|
||||||
label: 'share'.t(context: context),
|
label: 'share'.t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
const ShareActionButton(),
|
const ShareActionButton(source: ActionSource.viewer),
|
||||||
const _EditActionButton(),
|
const _EditActionButton(),
|
||||||
if (asset.hasRemote && isOwner)
|
if (asset.hasRemote && isOwner)
|
||||||
const ArchiveActionButton(source: ActionSource.viewer),
|
const ArchiveActionButton(source: ActionSource.viewer),
|
||||||
|
@ -45,7 +45,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
const ShareActionButton(),
|
const ShareActionButton(source: ActionSource.viewer),
|
||||||
if (asset.hasRemote) ...[
|
if (asset.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.viewer),
|
const ShareLinkActionButton(source: ActionSource.viewer),
|
||||||
const ArchiveActionButton(source: ActionSource.viewer),
|
const ArchiveActionButton(source: ActionSource.viewer),
|
||||||
|
@ -33,7 +33,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
|||||||
maxChildSize: 0.4,
|
maxChildSize: 0.4,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
const ShareActionButton(),
|
const ShareActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasRemote) ...[
|
if (multiselect.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const UnArchiveActionButton(source: ActionSource.timeline),
|
const UnArchiveActionButton(source: ActionSource.timeline),
|
||||||
|
@ -33,7 +33,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
|||||||
maxChildSize: 0.4,
|
maxChildSize: 0.4,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
const ShareActionButton(),
|
const ShareActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasRemote) ...[
|
if (multiselect.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const UnFavoriteActionButton(source: ActionSource.timeline),
|
const UnFavoriteActionButton(source: ActionSource.timeline),
|
||||||
|
@ -33,7 +33,7 @@ class GeneralBottomSheet extends ConsumerWidget {
|
|||||||
maxChildSize: 0.4,
|
maxChildSize: 0.4,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
const ShareActionButton(),
|
const ShareActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasRemote) ...[
|
if (multiselect.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const ArchiveActionButton(source: ActionSource.timeline),
|
const ArchiveActionButton(source: ActionSource.timeline),
|
||||||
|
@ -16,7 +16,7 @@ class LocalAlbumBottomSheet extends ConsumerWidget {
|
|||||||
maxChildSize: 0.4,
|
maxChildSize: 0.4,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
ShareActionButton(),
|
ShareActionButton(source: ActionSource.timeline),
|
||||||
DeleteLocalActionButton(source: ActionSource.timeline),
|
DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
UploadActionButton(),
|
UploadActionButton(),
|
||||||
],
|
],
|
||||||
|
@ -17,7 +17,7 @@ class LockedFolderBottomSheet extends ConsumerWidget {
|
|||||||
maxChildSize: 0.4,
|
maxChildSize: 0.4,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
ShareActionButton(),
|
ShareActionButton(source: ActionSource.timeline),
|
||||||
DownloadActionButton(),
|
DownloadActionButton(),
|
||||||
DeletePermanentActionButton(source: ActionSource.timeline),
|
DeletePermanentActionButton(source: ActionSource.timeline),
|
||||||
RemoveFromLockFolderActionButton(source: ActionSource.timeline),
|
RemoveFromLockFolderActionButton(source: ActionSource.timeline),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
@ -14,7 +15,7 @@ class PartnerDetailBottomSheet extends ConsumerWidget {
|
|||||||
maxChildSize: 0.4,
|
maxChildSize: 0.4,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
ShareActionButton(),
|
ShareActionButton(source: ActionSource.timeline),
|
||||||
DownloadActionButton(),
|
DownloadActionButton(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -36,7 +36,7 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
|
|||||||
maxChildSize: 0.4,
|
maxChildSize: 0.4,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
const ShareActionButton(),
|
const ShareActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasRemote) ...[
|
if (multiselect.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const ArchiveActionButton(source: ActionSource.timeline),
|
const ArchiveActionButton(source: ActionSource.timeline),
|
||||||
|
@ -58,15 +58,18 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Iterable<T> _getIdsForSource<T extends BaseAsset>(ActionSource source) {
|
Set<BaseAsset> _getAssets(ActionSource source) {
|
||||||
final Set<BaseAsset> assets = switch (source) {
|
return switch (source) {
|
||||||
ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets,
|
ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets,
|
||||||
ActionSource.viewer => switch (ref.read(currentAssetNotifier)) {
|
ActionSource.viewer => switch (ref.read(currentAssetNotifier)) {
|
||||||
BaseAsset asset => {asset},
|
BaseAsset asset => {asset},
|
||||||
null => const {},
|
null => const {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<T> _getIdsForSource<T extends BaseAsset>(ActionSource source) {
|
||||||
|
final Set<BaseAsset> assets = _getAssets(source);
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
const (RemoteAsset) => assets.whereType<RemoteAsset>(),
|
const (RemoteAsset) => assets.whereType<RemoteAsset>(),
|
||||||
const (LocalAsset) => assets.whereType<LocalAsset>(),
|
const (LocalAsset) => assets.whereType<LocalAsset>(),
|
||||||
@ -266,6 +269,22 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ActionResult> shareAssets(ActionSource source) async {
|
||||||
|
final ids = _getAssets(source).toList(growable: false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final count = await _service.shareAssets(ids);
|
||||||
|
return ActionResult(count: count, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to share assets', error, stack);
|
||||||
|
return ActionResult(
|
||||||
|
count: ids.length,
|
||||||
|
success: false,
|
||||||
|
error: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on Iterable<RemoteAsset> {
|
extension on Iterable<RemoteAsset> {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
@ -83,6 +84,10 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Response> downloadAsset(String id) {
|
||||||
|
return _api.downloadAssetWithHttpInfo(id);
|
||||||
|
}
|
||||||
|
|
||||||
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) {
|
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) {
|
||||||
AssetVisibilityEnum.timeline => AssetVisibility.timeline,
|
AssetVisibilityEnum.timeline => AssetVisibility.timeline,
|
||||||
AssetVisibilityEnum.hidden => AssetVisibility.hidden,
|
AssetVisibilityEnum.hidden => AssetVisibility.hidden,
|
||||||
|
@ -1,27 +1,40 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity;
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||||
import 'package:immich_mobile/utils/hash.dart';
|
import 'package:immich_mobile/utils/hash.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart' hide AssetType;
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/response_extensions.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
final assetMediaRepositoryProvider =
|
final assetMediaRepositoryProvider = Provider(
|
||||||
Provider((ref) => const AssetMediaRepository());
|
(ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
class AssetMediaRepository {
|
class AssetMediaRepository {
|
||||||
const AssetMediaRepository();
|
final AssetApiRepository _assetApiRepository;
|
||||||
|
static final Logger _log = Logger("AssetMediaRepository");
|
||||||
|
|
||||||
|
const AssetMediaRepository(this._assetApiRepository);
|
||||||
|
|
||||||
Future<List<String>> deleteAll(List<String> ids) =>
|
Future<List<String>> deleteAll(List<String> ids) =>
|
||||||
PhotoManager.editor.deleteWithIds(ids);
|
PhotoManager.editor.deleteWithIds(ids);
|
||||||
|
|
||||||
Future<Asset?> get(String id) async {
|
Future<asset_entity.Asset?> get(String id) async {
|
||||||
final entity = await AssetEntity.fromId(id);
|
final entity = await AssetEntity.fromId(id);
|
||||||
return toAsset(entity);
|
return toAsset(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Asset? toAsset(AssetEntity? local) {
|
static asset_entity.Asset? toAsset(AssetEntity? local) {
|
||||||
if (local == null) return null;
|
if (local == null) return null;
|
||||||
final Asset asset = Asset(
|
final asset_entity.Asset asset = asset_entity.Asset(
|
||||||
checksum: "",
|
checksum: "",
|
||||||
localId: local.id,
|
localId: local.id,
|
||||||
ownerId: fastHash(Store.get(StoreKey.currentUser).id),
|
ownerId: fastHash(Store.get(StoreKey.currentUser).id),
|
||||||
@ -29,7 +42,7 @@ class AssetMediaRepository {
|
|||||||
fileModifiedAt: local.modifiedDateTime,
|
fileModifiedAt: local.modifiedDateTime,
|
||||||
updatedAt: local.modifiedDateTime,
|
updatedAt: local.modifiedDateTime,
|
||||||
durationInSeconds: local.duration,
|
durationInSeconds: local.duration,
|
||||||
type: AssetType.values[local.typeInt],
|
type: asset_entity.AssetType.values[local.typeInt],
|
||||||
fileName: local.title!,
|
fileName: local.title!,
|
||||||
width: local.width,
|
width: local.width,
|
||||||
height: local.height,
|
height: local.height,
|
||||||
@ -57,4 +70,57 @@ class AssetMediaRepository {
|
|||||||
// otherwise using the `entity.title` would return a random GUID
|
// otherwise using the `entity.title` would return a random GUID
|
||||||
return await entity.titleAsync;
|
return await entity.titleAsync;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: make this more efficient
|
||||||
|
Future<int> shareAssets(List<BaseAsset> assets) async {
|
||||||
|
final downloadedXFiles = <XFile>[];
|
||||||
|
|
||||||
|
for (var asset in assets) {
|
||||||
|
final localId = (asset is LocalAsset)
|
||||||
|
? asset.id
|
||||||
|
: asset is RemoteAsset
|
||||||
|
? asset.localId
|
||||||
|
: null;
|
||||||
|
if (localId != null) {
|
||||||
|
File? f =
|
||||||
|
await AssetEntity(id: localId, width: 1, height: 1, typeInt: 0)
|
||||||
|
.originFile;
|
||||||
|
downloadedXFiles.add(XFile(f!.path));
|
||||||
|
} else if (asset is RemoteAsset) {
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
final name = asset.name;
|
||||||
|
final tempFile = await File('${tempDir.path}/$name').create();
|
||||||
|
final res = await _assetApiRepository.downloadAsset(asset.id);
|
||||||
|
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
_log.severe("Download for $name failed", res.toLoggerString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await tempFile.writeAsBytes(res.bodyBytes);
|
||||||
|
downloadedXFiles.add(XFile(tempFile.path));
|
||||||
|
} else {
|
||||||
|
_log.warning("Asset type not supported for sharing: $asset");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadedXFiles.isEmpty) {
|
||||||
|
_log.warning("No asset can be retrieved for share");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await Share.shareXFiles(downloadedXFiles);
|
||||||
|
|
||||||
|
for (var file in downloadedXFiles) {
|
||||||
|
try {
|
||||||
|
await File(file.path).delete();
|
||||||
|
} catch (e) {
|
||||||
|
_log.warning("Failed to delete temporary file: ${file.path}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.status == ShareResultStatus.success
|
||||||
|
? downloadedXFiles.length
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
|||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart' as maplibre;
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
final actionServiceProvider = Provider<ActionService>(
|
final actionServiceProvider = Provider<ActionService>(
|
||||||
@ -124,12 +124,12 @@ class ActionService {
|
|||||||
List<String> remoteIds,
|
List<String> remoteIds,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
) async {
|
) async {
|
||||||
LatLng? initialLatLng;
|
maplibre.LatLng? initialLatLng;
|
||||||
if (remoteIds.length == 1) {
|
if (remoteIds.length == 1) {
|
||||||
final exif = await _remoteAssetRepository.getExif(remoteIds[0]);
|
final exif = await _remoteAssetRepository.getExif(remoteIds[0]);
|
||||||
|
|
||||||
if (exif?.latitude != null && exif?.longitude != null) {
|
if (exif?.latitude != null && exif?.longitude != null) {
|
||||||
initialLatLng = LatLng(exif!.latitude!, exif.longitude!);
|
initialLatLng = maplibre.LatLng(exif!.latitude!, exif.longitude!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,4 +165,8 @@ class ActionService {
|
|||||||
|
|
||||||
return removedCount;
|
return removedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> shareAssets(List<BaseAsset> assets) {
|
||||||
|
return _assetMediaRepository.shareAssets(assets);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user