mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(mobile): shared-links (#4490)
* add shared links page * feat(mobile): shared link items * feat(mobile): create / edit shared links page * server: add changeExpiryTime to SharedLinkEditDto * fix(mobile): edit expiry to never * mobile: add icon when shares list is empty * mobile: create new share from album / timeline * mobile: add translation texts * mobile: minor ui fixes * fix: handle serverURL with /api path * mobile: show share link on successful creation * mobile: shared links list - 2 column layout * mobile: use sharedlink pod class instead of dto * mobile: show error on link creation * mobile: show share icon only when remote assets are in selection * mobile: use server endpoint instead of server url * styling * styling --------- Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -210,6 +210,18 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.share_rounded),
|
||||
onTap: () {
|
||||
AutoRouter.of(context)
|
||||
.push(SharedLinkEditRoute(albumId: album.remoteId));
|
||||
Navigator.pop(context);
|
||||
},
|
||||
title: const Text(
|
||||
"control_bottom_app_bar_share",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings_rounded),
|
||||
onTap: () =>
|
||||
|
||||
@@ -147,13 +147,13 @@ class SharingPage extends HookConsumerWidget {
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () =>
|
||||
AutoRouter.of(context).push(const PartnerRoute()),
|
||||
AutoRouter.of(context).push(const SharedLinkRoute()),
|
||||
icon: const Icon(
|
||||
Icons.swap_horizontal_circle_outlined,
|
||||
Icons.link,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text(
|
||||
"sharing_silver_appbar_share_partner",
|
||||
"sharing_silver_appbar_shared_links",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
@@ -179,6 +179,17 @@ class SharingPage extends HookConsumerWidget {
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
splashRadius: 25,
|
||||
iconSize: 20,
|
||||
icon: const Icon(
|
||||
Icons.swap_horizontal_circle_outlined,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => AutoRouter.of(context).push(const PartnerRoute()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -12,7 +10,7 @@ import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
|
||||
class ControlBottomAppBar extends ConsumerWidget {
|
||||
final void Function() onShare;
|
||||
final void Function(bool shareLocal) onShare;
|
||||
final void Function() onFavorite;
|
||||
final void Function() onArchive;
|
||||
final void Function() onDelete;
|
||||
@@ -51,73 +49,73 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||
final trashEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||
|
||||
Widget renderActionButtons() {
|
||||
return Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 15,
|
||||
children: [
|
||||
List<Widget> renderActionButtons() {
|
||||
return [
|
||||
if (hasRemote)
|
||||
ControlBoxButton(
|
||||
iconData: Platform.isAndroid
|
||||
? Icons.share_rounded
|
||||
: Icons.ios_share_rounded,
|
||||
iconData: Icons.share_rounded,
|
||||
label: "control_bottom_app_bar_share".tr(),
|
||||
onPressed: enabled ? onShare : null,
|
||||
onPressed: enabled ? () => onShare(false) : null,
|
||||
),
|
||||
if (hasRemote)
|
||||
ControlBoxButton(
|
||||
iconData: Icons.archive,
|
||||
label: "control_bottom_app_bar_archive".tr(),
|
||||
onPressed: enabled ? onArchive : null,
|
||||
),
|
||||
if (hasRemote)
|
||||
ControlBoxButton(
|
||||
iconData: Icons.favorite_border_rounded,
|
||||
label: "control_bottom_app_bar_favorite".tr(),
|
||||
onPressed: enabled ? onFavorite : null,
|
||||
),
|
||||
ControlBoxButton(
|
||||
iconData: Icons.ios_share_rounded,
|
||||
label: "control_bottom_app_bar_share_to".tr(),
|
||||
onPressed: enabled ? () => onShare(true) : null,
|
||||
),
|
||||
if (hasRemote)
|
||||
ControlBoxButton(
|
||||
iconData: Icons.delete_outline_rounded,
|
||||
label: "control_bottom_app_bar_delete".tr(),
|
||||
onPressed: enabled
|
||||
? () {
|
||||
if (!trashEnabled) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return DeleteDialog(
|
||||
onDelete: onDelete,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
onDelete();
|
||||
}
|
||||
iconData: Icons.archive,
|
||||
label: "control_bottom_app_bar_archive".tr(),
|
||||
onPressed: enabled ? onArchive : null,
|
||||
),
|
||||
if (hasRemote)
|
||||
ControlBoxButton(
|
||||
iconData: Icons.favorite_border_rounded,
|
||||
label: "control_bottom_app_bar_favorite".tr(),
|
||||
onPressed: enabled ? onFavorite : null,
|
||||
),
|
||||
ControlBoxButton(
|
||||
iconData: Icons.delete_outline_rounded,
|
||||
label: "control_bottom_app_bar_delete".tr(),
|
||||
onPressed: enabled
|
||||
? () {
|
||||
if (!trashEnabled) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return DeleteDialog(
|
||||
onDelete: onDelete,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
onDelete();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
if (!hasLocal)
|
||||
ControlBoxButton(
|
||||
iconData: Icons.filter_none_rounded,
|
||||
label: "control_bottom_app_bar_stack".tr(),
|
||||
onPressed: enabled ? onStack : null,
|
||||
),
|
||||
if (!hasRemote)
|
||||
ControlBoxButton(
|
||||
iconData: Icons.backup_outlined,
|
||||
label: "Upload",
|
||||
onPressed: enabled
|
||||
? () => showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return UploadDialog(
|
||||
onUpload: onUpload,
|
||||
);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (!hasRemote)
|
||||
ControlBoxButton(
|
||||
iconData: Icons.backup_outlined,
|
||||
label: "control_bottom_app_bar_upload".tr(),
|
||||
onPressed: enabled
|
||||
? () => showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return UploadDialog(
|
||||
onUpload: onUpload,
|
||||
);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (!hasLocal)
|
||||
ControlBoxButton(
|
||||
iconData: Icons.filter_none_rounded,
|
||||
label: "control_bottom_app_bar_stack".tr(),
|
||||
onPressed: enabled ? onStack : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
return DraggableScrollableSheet(
|
||||
@@ -149,7 +147,13 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||
const SizedBox(height: 12),
|
||||
const CustomDraggingHandle(),
|
||||
const SizedBox(height: 12),
|
||||
renderActionButtons(),
|
||||
SizedBox(
|
||||
height: 70,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: renderActionButtons(),
|
||||
),
|
||||
),
|
||||
if (hasRemote)
|
||||
const Divider(
|
||||
indent: 16,
|
||||
@@ -173,10 +177,6 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||
enabled: enabled,
|
||||
),
|
||||
),
|
||||
if (hasRemote)
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(height: 200),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -209,7 +209,10 @@ class AddToAlbumTitleRow extends StatelessWidget {
|
||||
).tr(),
|
||||
TextButton.icon(
|
||||
onPressed: onCreateNewAlbum,
|
||||
icon: const Icon(Icons.add),
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
label: Text(
|
||||
"common_create_new_album",
|
||||
style: TextStyle(
|
||||
|
||||
@@ -91,12 +91,6 @@ class HomePage extends HookConsumerWidget {
|
||||
SelectionAssetState.fromSelection(selectedAssets);
|
||||
}
|
||||
|
||||
void onShareAssets() {
|
||||
handleShareAssets(ref, context, selection.value.toList());
|
||||
|
||||
selectionEnabledHook.value = false;
|
||||
}
|
||||
|
||||
List<Asset> remoteOnlySelection({String? localErrorMessage}) {
|
||||
final Set<Asset> assets = selection.value;
|
||||
final bool onlyRemote = assets.every((e) => e.isRemote);
|
||||
@@ -113,6 +107,19 @@ class HomePage extends HookConsumerWidget {
|
||||
return assets.toList();
|
||||
}
|
||||
|
||||
void onShareAssets(bool shareLocal) {
|
||||
processing.value = true;
|
||||
if (shareLocal) {
|
||||
handleShareAssets(ref, context, selection.value.toList());
|
||||
} else {
|
||||
final ids = remoteOnlySelection().map((e) => e.remoteId!);
|
||||
AutoRouter.of(context)
|
||||
.push(SharedLinkEditRoute(assetsList: ids.toList()));
|
||||
}
|
||||
processing.value = false;
|
||||
selectionEnabledHook.value = false;
|
||||
}
|
||||
|
||||
void onFavoriteAssets() async {
|
||||
processing.value = true;
|
||||
try {
|
||||
|
||||
107
mobile/lib/modules/shared_link/models/shared_link.dart
Normal file
107
mobile/lib/modules/shared_link/models/shared_link.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
enum SharedLinkSource { album, individual }
|
||||
|
||||
class SharedLink {
|
||||
final String id;
|
||||
final String title;
|
||||
final bool allowDownload;
|
||||
final bool allowUpload;
|
||||
final String? thumbAssetId;
|
||||
final String? description;
|
||||
final DateTime? expiresAt;
|
||||
final String key;
|
||||
final bool showMetadata;
|
||||
final SharedLinkSource type;
|
||||
|
||||
const SharedLink({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.allowDownload,
|
||||
required this.allowUpload,
|
||||
required this.thumbAssetId,
|
||||
required this.description,
|
||||
required this.expiresAt,
|
||||
required this.key,
|
||||
required this.showMetadata,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
SharedLink copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? thumbAssetId,
|
||||
bool? allowDownload,
|
||||
bool? allowUpload,
|
||||
String? description,
|
||||
DateTime? expiresAt,
|
||||
String? key,
|
||||
bool? showMetadata,
|
||||
SharedLinkSource? type,
|
||||
}) {
|
||||
return SharedLink(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
thumbAssetId: thumbAssetId ?? this.thumbAssetId,
|
||||
allowDownload: allowDownload ?? this.allowDownload,
|
||||
allowUpload: allowUpload ?? this.allowUpload,
|
||||
description: description ?? this.description,
|
||||
expiresAt: expiresAt ?? this.expiresAt,
|
||||
key: key ?? this.key,
|
||||
showMetadata: showMetadata ?? this.showMetadata,
|
||||
type: type ?? this.type,
|
||||
);
|
||||
}
|
||||
|
||||
SharedLink.fromDto(SharedLinkResponseDto dto)
|
||||
: id = dto.id,
|
||||
allowDownload = dto.allowDownload,
|
||||
allowUpload = dto.allowUpload,
|
||||
description = dto.description,
|
||||
expiresAt = dto.expiresAt,
|
||||
key = dto.key,
|
||||
showMetadata = dto.showMetadata,
|
||||
type = dto.type == SharedLinkType.ALBUM
|
||||
? SharedLinkSource.album
|
||||
: SharedLinkSource.individual,
|
||||
title = dto.type == SharedLinkType.ALBUM
|
||||
? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE"
|
||||
: "INDIVIDUAL SHARE",
|
||||
thumbAssetId = dto.type == SharedLinkType.ALBUM
|
||||
? dto.album?.albumThumbnailAssetId
|
||||
: dto.assets.isNotEmpty
|
||||
? dto.assets[0].id
|
||||
: null;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SharedLink &&
|
||||
other.id == id &&
|
||||
other.title == title &&
|
||||
other.thumbAssetId == thumbAssetId &&
|
||||
other.allowDownload == allowDownload &&
|
||||
other.allowUpload == allowUpload &&
|
||||
other.description == description &&
|
||||
other.expiresAt == expiresAt &&
|
||||
other.key == key &&
|
||||
other.showMetadata == showMetadata &&
|
||||
other.type == type;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
id.hashCode ^
|
||||
title.hashCode ^
|
||||
thumbAssetId.hashCode ^
|
||||
allowDownload.hashCode ^
|
||||
allowUpload.hashCode ^
|
||||
description.hashCode ^
|
||||
expiresAt.hashCode ^
|
||||
key.hashCode ^
|
||||
showMetadata.hashCode ^
|
||||
type.hashCode;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/services/shared_link.service.dart';
|
||||
|
||||
class SharedLinksNotifier extends StateNotifier<AsyncValue<List<SharedLink>>> {
|
||||
final SharedLinkService _sharedLinkService;
|
||||
|
||||
SharedLinksNotifier(this._sharedLinkService) : super(const AsyncLoading()) {
|
||||
fetchLinks();
|
||||
}
|
||||
|
||||
Future<void> fetchLinks() async {
|
||||
state = await _sharedLinkService.getAllSharedLinks();
|
||||
}
|
||||
|
||||
Future<void> deleteLink(String id) async {
|
||||
await _sharedLinkService.deleteSharedLink(id);
|
||||
state = const AsyncLoading();
|
||||
fetchLinks();
|
||||
}
|
||||
}
|
||||
|
||||
final sharedLinksStateProvider =
|
||||
StateNotifierProvider<SharedLinksNotifier, AsyncValue<List<SharedLink>>>(
|
||||
(ref) {
|
||||
return SharedLinksNotifier(
|
||||
ref.watch(sharedLinkServiceProvider),
|
||||
);
|
||||
});
|
||||
115
mobile/lib/modules/shared_link/services/shared_link.service.dart
Normal file
115
mobile/lib/modules/shared_link/services/shared_link.service.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
|
||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final sharedLinkServiceProvider = Provider(
|
||||
(ref) => SharedLinkService(ref.watch(apiServiceProvider)),
|
||||
);
|
||||
|
||||
class SharedLinkService {
|
||||
final ApiService _apiService;
|
||||
final Logger _log = Logger("SharedLinkService");
|
||||
|
||||
SharedLinkService(this._apiService);
|
||||
|
||||
Future<AsyncValue<List<SharedLink>>> getAllSharedLinks() async {
|
||||
try {
|
||||
final list = await _apiService.sharedLinkApi.getAllSharedLinks();
|
||||
return list != null
|
||||
? AsyncData(list.map(SharedLink.fromDto).toList())
|
||||
: const AsyncData([]);
|
||||
} catch (e, stack) {
|
||||
_log.severe("failed to fetch shared links - $e");
|
||||
return AsyncError(e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteSharedLink(String id) async {
|
||||
try {
|
||||
return await _apiService.sharedLinkApi.removeSharedLink(id);
|
||||
} catch (e) {
|
||||
_log.severe("failed to delete shared link id - $id with error - $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<SharedLink?> createSharedLink({
|
||||
required bool showMeta,
|
||||
required bool allowDownload,
|
||||
required bool allowUpload,
|
||||
String? description,
|
||||
String? albumId,
|
||||
List<String>? assetIds,
|
||||
DateTime? expiresAt,
|
||||
}) async {
|
||||
try {
|
||||
final type =
|
||||
albumId != null ? SharedLinkType.ALBUM : SharedLinkType.INDIVIDUAL;
|
||||
SharedLinkCreateDto? dto;
|
||||
if (type == SharedLinkType.ALBUM) {
|
||||
dto = SharedLinkCreateDto(
|
||||
type: type,
|
||||
albumId: albumId,
|
||||
showMetadata: showMeta,
|
||||
allowDownload: allowDownload,
|
||||
allowUpload: allowUpload,
|
||||
expiresAt: expiresAt,
|
||||
description: description,
|
||||
);
|
||||
} else if (assetIds != null) {
|
||||
dto = SharedLinkCreateDto(
|
||||
type: type,
|
||||
showMetadata: showMeta,
|
||||
allowDownload: allowDownload,
|
||||
allowUpload: allowUpload,
|
||||
expiresAt: expiresAt,
|
||||
description: description,
|
||||
assetIds: assetIds,
|
||||
);
|
||||
}
|
||||
|
||||
if (dto != null) {
|
||||
final responseDto =
|
||||
await _apiService.sharedLinkApi.createSharedLink(dto);
|
||||
if (responseDto != null) {
|
||||
return SharedLink.fromDto(responseDto);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("failed to create shared link with error - $e");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<SharedLink?> updateSharedLink(
|
||||
String id, {
|
||||
required bool? showMeta,
|
||||
required bool? allowDownload,
|
||||
required bool? allowUpload,
|
||||
bool? changeExpiry = false,
|
||||
String? description,
|
||||
DateTime? expiresAt,
|
||||
}) async {
|
||||
try {
|
||||
final responseDto = await _apiService.sharedLinkApi.updateSharedLink(
|
||||
id,
|
||||
SharedLinkEditDto(
|
||||
showMetadata: showMeta,
|
||||
allowDownload: allowDownload,
|
||||
allowUpload: allowUpload,
|
||||
expiresAt: expiresAt,
|
||||
description: description,
|
||||
changeExpiryTime: changeExpiry,
|
||||
),
|
||||
);
|
||||
if (responseDto != null) {
|
||||
return SharedLink.fromDto(responseDto);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("failed to update shared link id - $id with error - $e");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
307
mobile/lib/modules/shared_link/ui/shared_link_item.dart
Normal file
307
mobile/lib/modules/shared_link/ui/shared_link_item.dart
Normal file
@@ -0,0 +1,307 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
|
||||
class SharedLinkItem extends ConsumerWidget {
|
||||
final SharedLink sharedLink;
|
||||
|
||||
const SharedLinkItem(this.sharedLink, {super.key});
|
||||
|
||||
bool isExpired() {
|
||||
if (sharedLink.expiresAt != null) {
|
||||
return DateTime.now().isAfter(sharedLink.expiresAt!);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Widget getExpiryDuration(bool isDarkMode) {
|
||||
var expiresText = "Expires ∞";
|
||||
if (sharedLink.expiresAt != null) {
|
||||
if (isExpired()) {
|
||||
return Text(
|
||||
"Expired",
|
||||
style: TextStyle(color: Colors.red[300]),
|
||||
);
|
||||
}
|
||||
final difference = sharedLink.expiresAt!.difference(DateTime.now());
|
||||
debugPrint("Difference: $difference");
|
||||
if (difference.inDays > 0) {
|
||||
var dayDifference = difference.inDays;
|
||||
if (difference.inHours % 24 > 12) {
|
||||
dayDifference += 1;
|
||||
}
|
||||
expiresText = "in $dayDifference days";
|
||||
} else if (difference.inHours > 0) {
|
||||
expiresText = "in ${difference.inHours} hours";
|
||||
} else if (difference.inMinutes > 0) {
|
||||
expiresText = "in ${difference.inMinutes} minutes";
|
||||
} else if (difference.inSeconds > 0) {
|
||||
expiresText = "in ${difference.inSeconds} seconds";
|
||||
}
|
||||
}
|
||||
return Text(
|
||||
expiresText,
|
||||
style: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final themeData = Theme.of(context);
|
||||
final isDarkMode = themeData.brightness == Brightness.dark;
|
||||
final thumbnailUrl = sharedLink.thumbAssetId != null
|
||||
? getThumbnailUrlForRemoteId(sharedLink.thumbAssetId!)
|
||||
: null;
|
||||
final imageSize = math.min(MediaQuery.of(context).size.width / 4, 100.0);
|
||||
|
||||
void copyShareLinkToClipboard() {
|
||||
final serverUrl = getServerUrl();
|
||||
if (serverUrl == null) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastType: ToastType.error,
|
||||
msg: 'Cannot fetch the server url',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: "$serverUrl/share/${sharedLink.key}",
|
||||
),
|
||||
).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
"Copied to clipboard",
|
||||
),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteShareLink() async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ConfirmDialog(
|
||||
title: "delete_shared_link_dialog_title",
|
||||
content: "delete_shared_link_dialog_content",
|
||||
onOk: () => ref
|
||||
.read(sharedLinksStateProvider.notifier)
|
||||
.deleteLink(sharedLink.id),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildThumbnail() {
|
||||
if (thumbnailUrl == null) {
|
||||
return Container(
|
||||
height: imageSize * 1.2,
|
||||
width: imageSize,
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
color: isDarkMode ? Colors.grey[100] : Colors.grey[700],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
height: imageSize * 1.2,
|
||||
width: imageSize,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: ThumbnailWithInfo(
|
||||
imageUrl: thumbnailUrl,
|
||||
key: key,
|
||||
textInfo: '',
|
||||
noImageIcon: Icons.image_not_supported_outlined,
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildInfoChip(String labelText) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: Chip(
|
||||
backgroundColor: themeData.primaryColor,
|
||||
label: Text(
|
||||
labelText,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDarkMode ? Colors.black : Colors.white,
|
||||
),
|
||||
),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(25)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBottomInfo() {
|
||||
return Row(
|
||||
children: [
|
||||
if (sharedLink.allowUpload) buildInfoChip("Upload"),
|
||||
if (sharedLink.allowDownload) buildInfoChip("Download"),
|
||||
if (sharedLink.showMetadata) buildInfoChip("EXIF"),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSharedLinkActions() {
|
||||
const actionIconSize = 20.0;
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
splashRadius: 25,
|
||||
constraints: const BoxConstraints(),
|
||||
iconSize: actionIconSize,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
style: const ButtonStyle(
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap, // the '2023' part
|
||||
),
|
||||
onPressed: deleteShareLink,
|
||||
),
|
||||
IconButton(
|
||||
splashRadius: 25,
|
||||
constraints: const BoxConstraints(),
|
||||
iconSize: actionIconSize,
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
style: const ButtonStyle(
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap, // the '2023' part
|
||||
),
|
||||
onPressed: () => AutoRouter.of(context)
|
||||
.push(SharedLinkEditRoute(existingLink: sharedLink)),
|
||||
),
|
||||
IconButton(
|
||||
splashRadius: 25,
|
||||
constraints: const BoxConstraints(),
|
||||
iconSize: actionIconSize,
|
||||
icon: const Icon(Icons.copy_outlined),
|
||||
style: const ButtonStyle(
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap, // the '2023' part
|
||||
),
|
||||
onPressed: copyShareLinkToClipboard,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSharedLinkDetails() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
getExpiryDuration(isDarkMode),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 5),
|
||||
child: Tooltip(
|
||||
verticalOffset: 0,
|
||||
decoration: BoxDecoration(
|
||||
color: themeData.primaryColor.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
textStyle: TextStyle(
|
||||
color: isDarkMode ? Colors.black : Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
message: sharedLink.title,
|
||||
preferBelow: false,
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
child: Text(
|
||||
sharedLink.title,
|
||||
style: TextStyle(
|
||||
color: themeData.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Tooltip(
|
||||
verticalOffset: 0,
|
||||
decoration: BoxDecoration(
|
||||
color: themeData.primaryColor.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
textStyle: TextStyle(
|
||||
color: isDarkMode ? Colors.black : Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
message: sharedLink.description ?? "",
|
||||
preferBelow: false,
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
child: Text(
|
||||
sharedLink.description ?? "",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 15),
|
||||
child: buildSharedLinkActions(),
|
||||
),
|
||||
],
|
||||
),
|
||||
buildBottomInfo(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: buildThumbnail(),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: buildSharedLinkDetails(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Divider(
|
||||
height: 0,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
459
mobile/lib/modules/shared_link/views/shared_link_edit_page.dart
Normal file
459
mobile/lib/modules/shared_link/views/shared_link_edit_page.dart
Normal file
@@ -0,0 +1,459 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/services/shared_link.service.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
|
||||
class SharedLinkEditPage extends HookConsumerWidget {
|
||||
final SharedLink? existingLink;
|
||||
final List<String>? assetsList;
|
||||
final String? albumId;
|
||||
|
||||
const SharedLinkEditPage({
|
||||
super.key,
|
||||
this.existingLink,
|
||||
this.assetsList,
|
||||
this.albumId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
const padding = 20.0;
|
||||
final themeData = Theme.of(context);
|
||||
final descriptionController =
|
||||
useTextEditingController(text: existingLink?.description ?? "");
|
||||
final descriptionFocusNode = useFocusNode();
|
||||
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
||||
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
||||
final allowUpload = useState(existingLink?.allowUpload ?? false);
|
||||
final editExpiry = useState(false);
|
||||
final expiryAfter = useState(0);
|
||||
final newShareLink = useState("");
|
||||
|
||||
Widget buildLinkTitle() {
|
||||
if (existingLink != null) {
|
||||
if (existingLink!.type == SharedLinkSource.album) {
|
||||
return Row(
|
||||
children: [
|
||||
const Text(
|
||||
"Public album | ",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
existingLink!.title,
|
||||
style: TextStyle(
|
||||
color: themeData.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (existingLink!.type == SharedLinkSource.individual) {
|
||||
return Row(
|
||||
children: [
|
||||
const Text(
|
||||
"Individual shared | ",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
existingLink!.description ?? "--",
|
||||
style: TextStyle(
|
||||
color: themeData.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return const Text(
|
||||
"shared_link_create_info",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
).tr();
|
||||
}
|
||||
|
||||
Widget buildDescriptionField() {
|
||||
return TextField(
|
||||
controller: descriptionController,
|
||||
enabled: newShareLink.value.isEmpty,
|
||||
focusNode: descriptionFocusNode,
|
||||
textInputAction: TextInputAction.done,
|
||||
autofocus: false,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'shared_link_edit_description'.tr(),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: themeData.primaryColor,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: 'shared_link_edit_description_hint'.tr(),
|
||||
hintStyle: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey.withOpacity(0.5)),
|
||||
),
|
||||
),
|
||||
onTapOutside: (_) => descriptionFocusNode.unfocus(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildShowMetaButton() {
|
||||
return SwitchListTile.adaptive(
|
||||
value: showMetadata.value,
|
||||
onChanged: newShareLink.value.isEmpty
|
||||
? (value) => showMetadata.value = value
|
||||
: null,
|
||||
activeColor: themeData.primaryColor,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"shared_link_edit_show_meta",
|
||||
style: themeData.textTheme.labelLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAllowDownloadButton() {
|
||||
return SwitchListTile.adaptive(
|
||||
value: allowDownload.value,
|
||||
onChanged: newShareLink.value.isEmpty
|
||||
? (value) => allowDownload.value = value
|
||||
: null,
|
||||
activeColor: themeData.primaryColor,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"shared_link_edit_allow_download",
|
||||
style: themeData.textTheme.labelLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAllowUploadButton() {
|
||||
return SwitchListTile.adaptive(
|
||||
value: allowUpload.value,
|
||||
onChanged: newShareLink.value.isEmpty
|
||||
? (value) => allowUpload.value = value
|
||||
: null,
|
||||
activeColor: themeData.primaryColor,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"shared_link_edit_allow_upload",
|
||||
style: themeData.textTheme.labelLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildEditExpiryButton() {
|
||||
return SwitchListTile.adaptive(
|
||||
value: editExpiry.value,
|
||||
onChanged: newShareLink.value.isEmpty
|
||||
? (value) => editExpiry.value = value
|
||||
: null,
|
||||
activeColor: themeData.primaryColor,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"shared_link_edit_change_expiry",
|
||||
style: themeData.textTheme.labelLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildExpiryAfterButton() {
|
||||
return DropdownMenu(
|
||||
enableSearch: false,
|
||||
enableFilter: false,
|
||||
width: MediaQuery.of(context).size.width - 40,
|
||||
initialSelection: expiryAfter.value,
|
||||
enabled: newShareLink.value.isEmpty &&
|
||||
(existingLink == null || editExpiry.value),
|
||||
onSelected: (value) {
|
||||
expiryAfter.value = value!;
|
||||
},
|
||||
inputDecorationTheme: themeData.inputDecorationTheme.copyWith(
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey.withOpacity(0.5)),
|
||||
),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
dropdownMenuEntries: const [
|
||||
DropdownMenuEntry(value: 0, label: "Never"),
|
||||
DropdownMenuEntry(
|
||||
value: 30,
|
||||
label: '30 minutes',
|
||||
),
|
||||
DropdownMenuEntry(
|
||||
value: 60,
|
||||
label: '1 hour',
|
||||
),
|
||||
DropdownMenuEntry(
|
||||
value: 60 * 6,
|
||||
label: '6 hours',
|
||||
),
|
||||
DropdownMenuEntry(
|
||||
value: 60 * 24,
|
||||
label: '1 day',
|
||||
),
|
||||
DropdownMenuEntry(
|
||||
value: 60 * 24 * 7,
|
||||
label: '7 days',
|
||||
),
|
||||
DropdownMenuEntry(
|
||||
value: 60 * 24 * 30,
|
||||
label: '30 days',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void copyLinkToClipboard() {
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: newShareLink.value,
|
||||
),
|
||||
).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
"Copied to clipboard",
|
||||
),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildNewLinkField() {
|
||||
return Column(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
),
|
||||
child: Divider(),
|
||||
),
|
||||
TextFormField(
|
||||
readOnly: true,
|
||||
initialValue: newShareLink.value,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
enabledBorder: themeData.inputDecorationTheme.focusedBorder,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: copyLinkToClipboard,
|
||||
icon: const Icon(Icons.copy),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
AutoRouter.of(context).pop();
|
||||
},
|
||||
child: const Text(
|
||||
"Done",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
DateTime calculateExpiry() {
|
||||
return DateTime.now().add(Duration(minutes: expiryAfter.value));
|
||||
}
|
||||
|
||||
Future<void> handleNewLink() async {
|
||||
final newLink =
|
||||
await ref.read(sharedLinkServiceProvider).createSharedLink(
|
||||
albumId: albumId,
|
||||
assetIds: assetsList,
|
||||
showMeta: showMetadata.value,
|
||||
allowDownload: allowDownload.value,
|
||||
allowUpload: allowUpload.value,
|
||||
description: descriptionController.text.isEmpty
|
||||
? null
|
||||
: descriptionController.text,
|
||||
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
||||
);
|
||||
ref.invalidate(sharedLinksStateProvider);
|
||||
final serverUrl = getServerUrl();
|
||||
if (newLink != null && serverUrl != null) {
|
||||
newShareLink.value = "$serverUrl/share/${newLink.key}";
|
||||
copyLinkToClipboard();
|
||||
} else if (newLink == null) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastType: ToastType.error,
|
||||
msg: 'Error while creating shared link',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleEditLink() async {
|
||||
bool? download;
|
||||
bool? upload;
|
||||
bool? meta;
|
||||
String? desc;
|
||||
DateTime? expiry;
|
||||
bool? changeExpiry;
|
||||
|
||||
if (allowDownload.value != existingLink!.allowDownload) {
|
||||
download = allowDownload.value;
|
||||
}
|
||||
|
||||
if (allowUpload.value != existingLink!.allowUpload) {
|
||||
upload = allowUpload.value;
|
||||
}
|
||||
|
||||
if (showMetadata.value != existingLink!.showMetadata) {
|
||||
meta = showMetadata.value;
|
||||
}
|
||||
|
||||
if (descriptionController.text != existingLink!.description) {
|
||||
desc = descriptionController.text;
|
||||
}
|
||||
|
||||
if (editExpiry.value) {
|
||||
expiry = expiryAfter.value == 0 ? null : calculateExpiry();
|
||||
changeExpiry = true;
|
||||
}
|
||||
|
||||
await ref.read(sharedLinkServiceProvider).updateSharedLink(
|
||||
existingLink!.id,
|
||||
showMeta: meta,
|
||||
allowDownload: download,
|
||||
allowUpload: upload,
|
||||
description: desc,
|
||||
expiresAt: expiry,
|
||||
changeExpiry: changeExpiry,
|
||||
);
|
||||
ref.invalidate(sharedLinksStateProvider);
|
||||
AutoRouter.of(context).pop();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
existingLink == null
|
||||
? "shared_link_create_app_bar_title"
|
||||
: "shared_link_edit_app_bar_title",
|
||||
).tr(),
|
||||
elevation: 0,
|
||||
leading: const CloseButton(),
|
||||
centerTitle: false,
|
||||
),
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(padding),
|
||||
child: buildLinkTitle(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(padding),
|
||||
child: buildDescriptionField(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: padding,
|
||||
right: padding,
|
||||
bottom: padding,
|
||||
),
|
||||
child: buildShowMetaButton(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: padding,
|
||||
right: padding,
|
||||
bottom: padding,
|
||||
),
|
||||
child: buildAllowDownloadButton(),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: padding, right: 20, bottom: 20),
|
||||
child: buildAllowUploadButton(),
|
||||
),
|
||||
if (existingLink != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: padding,
|
||||
right: padding,
|
||||
bottom: padding,
|
||||
),
|
||||
child: buildEditExpiryButton(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: padding,
|
||||
right: padding,
|
||||
bottom: padding,
|
||||
),
|
||||
child: buildExpiryAfterButton(),
|
||||
),
|
||||
if (newShareLink.value.isEmpty)
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: padding + 10),
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
existingLink != null ? handleEditLink : handleNewLink,
|
||||
child: Text(
|
||||
existingLink != null
|
||||
? "shared_link_edit_submit_button"
|
||||
: "shared_link_create_submit_button",
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (newShareLink.value.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: padding,
|
||||
right: padding,
|
||||
),
|
||||
child: buildNewLinkField(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
126
mobile/lib/modules/shared_link/views/shared_link_page.dart
Normal file
126
mobile/lib/modules/shared_link/views/shared_link_page.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
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/modules/shared_link/models/shared_link.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/ui/shared_link_item.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
|
||||
class SharedLinkPage extends HookConsumerWidget {
|
||||
const SharedLinkPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final sharedLinks = ref.watch(sharedLinksStateProvider);
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
ref.read(sharedLinksStateProvider.notifier).fetchLinks();
|
||||
return () => ref.invalidate(sharedLinksStateProvider);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
Widget buildNoShares() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
||||
child: const Text(
|
||||
"shared_link_manage_links",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: const Text(
|
||||
"shared_link_empty",
|
||||
style: TextStyle(fontSize: 14),
|
||||
).tr(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.link_off,
|
||||
size: 100,
|
||||
color: Theme.of(context).iconTheme.color?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSharesList(List<SharedLink> links) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, top: 16.0, bottom: 30.0),
|
||||
child: const Text(
|
||||
"shared_link_manage_links",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth > 600) {
|
||||
// Two column
|
||||
return GridView.builder(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisExtent: 180,
|
||||
),
|
||||
itemCount: links.length,
|
||||
itemBuilder: (context, index) {
|
||||
return SharedLinkItem(links.elementAt(index));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Single column
|
||||
return ListView.builder(
|
||||
itemCount: links.length,
|
||||
itemBuilder: (context, index) {
|
||||
return SharedLinkItem(links.elementAt(index));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("shared_link_app_bar_title").tr(),
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: sharedLinks.when(
|
||||
data: (links) =>
|
||||
links.isNotEmpty ? buildSharesList(links) : buildNoShares(),
|
||||
error: (error, stackTrace) => buildNoShares(),
|
||||
loading: () => const Center(child: ImmichLoadingIndicator()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user