Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
5fc8859f3b
|
|||
|
e30e7adbe2
|
|||
|
68be4db160
|
|||
|
aa91e376ca
|
|||
|
caffb85588
|
|||
|
521b192205
|
|||
|
77ac0428ea
|
|||
|
88c8227c66
|
|||
|
b20d8350a8
|
|||
|
98b27bed0e
|
|||
|
3a7d8b1a0d
|
|||
|
b4801d6af6
|
|||
|
aab5b957af
|
|||
|
43d706a184
|
|||
|
98df275f88
|
@@ -233,6 +233,9 @@
|
|||||||
"pickFile": "Pick a file",
|
"pickFile": "Pick a file",
|
||||||
"uploading": "Uploading",
|
"uploading": "Uploading",
|
||||||
"uploadingProgress": "Uploading {} of {}",
|
"uploadingProgress": "Uploading {} of {}",
|
||||||
|
"upload": "Upload",
|
||||||
|
"uploadSuccess": "Upload successful!",
|
||||||
|
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||||
"uploadAll": "Upload All",
|
"uploadAll": "Upload All",
|
||||||
"stickerCopyPlaceholder": "Copy Placeholder",
|
"stickerCopyPlaceholder": "Copy Placeholder",
|
||||||
"realmSelection": "Select a Realm",
|
"realmSelection": "Select a Realm",
|
||||||
@@ -1110,7 +1113,6 @@
|
|||||||
"deleteRecycledFiles": "Delete Recycled Files",
|
"deleteRecycledFiles": "Delete Recycled Files",
|
||||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||||
"upload": "Upload",
|
|
||||||
"updateAvailable": "Update available",
|
"updateAvailable": "Update available",
|
||||||
"noChangelogProvided": "No changelog provided.",
|
"noChangelogProvided": "No changelog provided.",
|
||||||
"useSecondarySourceForDownload": "Use secondary source for download",
|
"useSecondarySourceForDownload": "Use secondary source for download",
|
||||||
@@ -1471,5 +1473,6 @@
|
|||||||
"allFilesUploadedSuccess": "All files uploaded successfully",
|
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||||
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||||
"lotteryMultiplierRequired": "Please enter a multiplier",
|
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||||
"lotteryMultiplierRange": "Multiplier must be between 1 and 10"
|
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
|
||||||
|
"dropToShare": "Drop to share"
|
||||||
}
|
}
|
||||||
@@ -585,10 +585,10 @@
|
|||||||
"unknownChat": "未知聊天",
|
"unknownChat": "未知聊天",
|
||||||
"addAdditionalMessage": "添加附加消息……",
|
"addAdditionalMessage": "添加附加消息……",
|
||||||
"uploadingFiles": "上传文件中……",
|
"uploadingFiles": "上传文件中……",
|
||||||
"sharedSuccessfully": "分享成功!",
|
"sharedSuccessfully": "分享成功",
|
||||||
"shareSuccess": "分享成功!",
|
"shareSuccess": "分享成功",
|
||||||
"shareToSpecificChatSuccess": "成功分享至 {}!",
|
"shareToSpecificChatSuccess": "成功分享至 {}",
|
||||||
"wouldYouLikeToGoToChat": "是否前往该聊天?",
|
"wouldYouLikeToGoToChat": "是否前往该聊天页面?",
|
||||||
"no": "否",
|
"no": "否",
|
||||||
"yes": "是",
|
"yes": "是",
|
||||||
"navigateToChat": "前往聊天",
|
"navigateToChat": "前往聊天",
|
||||||
|
|||||||
@@ -140,21 +140,29 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
|
|
||||||
guard !attachmentUrls.isEmpty else {
|
guard !attachmentUrls.isEmpty else {
|
||||||
print("Invalid URLs for attachments: \(attachmentUrls)")
|
print("Invalid URLs for attachments: \(attachmentUrls)")
|
||||||
|
self.contentHandler?(content)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetSize = 512
|
let targetSize = 512
|
||||||
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
||||||
|
|
||||||
|
let dispatchGroup = DispatchGroup()
|
||||||
|
var attachments: [UNNotificationAttachment] = []
|
||||||
|
let lock = NSLock() // To synchronize access to the attachments array
|
||||||
|
|
||||||
for attachmentUrl in attachmentUrls {
|
for attachmentUrl in attachmentUrls {
|
||||||
guard let remoteUrl = URL(string: attachmentUrl) else {
|
guard let remoteUrl = URL(string: attachmentUrl) else {
|
||||||
print("Invalid URL for attachment: \(attachmentUrl)")
|
print("Invalid URL for attachment: \(attachmentUrl)")
|
||||||
continue // Skip this URL and move to the next one
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatchGroup.enter()
|
||||||
|
|
||||||
KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [
|
KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [
|
||||||
.processor(scaleProcessor)
|
.processor(scaleProcessor)
|
||||||
] : nil) { [weak self] result in
|
] : nil) { [weak self] result in
|
||||||
|
defer { dispatchGroup.leave() }
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
@@ -166,49 +174,34 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
do {
|
do {
|
||||||
// Write the image data to a temporary file for UNNotificationAttachment
|
// Write the image data to a temporary file for UNNotificationAttachment
|
||||||
try retrievalResult.image.pngData()?.write(to: cachedFileUrl)
|
try retrievalResult.image.pngData()?.write(to: cachedFileUrl)
|
||||||
self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: attachmentUrl)
|
|
||||||
|
if let attachment = try? UNNotificationAttachment(identifier: attachmentUrl, url: cachedFileUrl, options: [
|
||||||
|
UNNotificationAttachmentOptionsTypeHintKey: type?.identifier as Any,
|
||||||
|
UNNotificationAttachmentOptionsThumbnailHiddenKey: 0,
|
||||||
|
]) {
|
||||||
|
lock.lock()
|
||||||
|
attachments.append(attachment)
|
||||||
|
lock.unlock()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to write media to temporary file: \(error.localizedDescription)")
|
print("Failed to write media to temporary file: \(error.localizedDescription)")
|
||||||
self.contentHandler?(content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
print("Failed to retrieve image: \(error.localizedDescription)")
|
print("Failed to retrieve image: \(error.localizedDescription)")
|
||||||
self.contentHandler?(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func attachLocalMedia(to content: UNMutableNotificationContent, fileType type: String?, from localUrl: URL, withIdentifier identifier: String) {
|
|
||||||
do {
|
|
||||||
let attachment = try UNNotificationAttachment(identifier: identifier, url: localUrl, options: [
|
|
||||||
UNNotificationAttachmentOptionsTypeHintKey: type as Any,
|
|
||||||
UNNotificationAttachmentOptionsThumbnailHiddenKey: 0,
|
|
||||||
])
|
|
||||||
content.attachments = [attachment]
|
|
||||||
} catch let error as NSError {
|
|
||||||
// Log detailed error information
|
|
||||||
print("Failed to create attachment from file at \(localUrl.path)")
|
|
||||||
print("Error: \(error.localizedDescription)")
|
|
||||||
|
|
||||||
// Check specific error codes if needed
|
|
||||||
if error.domain == NSCocoaErrorDomain {
|
|
||||||
switch error.code {
|
|
||||||
case NSFileReadNoSuchFileError:
|
|
||||||
print("File does not exist at \(localUrl.path)")
|
|
||||||
case NSFileReadNoPermissionError:
|
|
||||||
print("No permission to read file at \(localUrl.path)")
|
|
||||||
default:
|
|
||||||
print("Unhandled file error: \(error.code)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call content handler regardless of success or failure
|
dispatchGroup.notify(queue: .main) { [weak self] in
|
||||||
self.contentHandler?(content)
|
guard let self = self else { return }
|
||||||
|
content.attachments = attachments
|
||||||
|
self.contentHandler?(content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private func createMessageIntent(with sender: INPerson, meta: [AnyHashable: Any], body: String) -> INSendMessageIntent {
|
private func createMessageIntent(with sender: INPerson, meta: [AnyHashable: Any], body: String) -> INSendMessageIntent {
|
||||||
INSendMessageIntent(
|
INSendMessageIntent(
|
||||||
recipients: nil,
|
recipients: nil,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'accountDeletionHint'.tr(),
|
'accountDeletionHint'.tr(),
|
||||||
'accountDeletion'.tr(),
|
'accountDeletion'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirm || !context.mounted) return;
|
if (!confirm || !context.mounted) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class AuthFactorSheet extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'authFactorDeleteHint'.tr(),
|
'authFactorDeleteHint'.tr(),
|
||||||
'authFactorDelete'.tr(),
|
'authFactorDelete'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirm || !context.mounted) return;
|
if (!confirm || !context.mounted) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class AccountConnectionSheet extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'accountConnectionDeleteHint'.tr(),
|
'accountConnectionDeleteHint'.tr(),
|
||||||
'accountConnectionDelete'.tr(),
|
'accountConnectionDelete'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirm || !context.mounted) return;
|
if (!confirm || !context.mounted) return;
|
||||||
try {
|
try {
|
||||||
@@ -332,6 +333,7 @@ class AccountConnectionsSheet extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'accountConnectionDeleteHint'.tr(),
|
'accountConnectionDeleteHint'.tr(),
|
||||||
'accountConnectionDelete'.tr(),
|
'accountConnectionDelete'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (confirm && context.mounted) {
|
if (confirm && context.mounted) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class ContactMethodSheet extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'contactMethodDeleteHint'.tr(),
|
'contactMethodDeleteHint'.tr(),
|
||||||
'contactMethodDelete'.tr(),
|
'contactMethodDelete'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirm || !context.mounted) return;
|
if (!confirm || !context.mounted) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import 'package:island/models/chat.dart';
|
|||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
import 'package:island/pods/database.dart';
|
import 'package:island/pods/database.dart';
|
||||||
import 'package:island/pods/chat/call.dart';
|
|
||||||
import 'package:island/pods/chat/chat_summary.dart';
|
import 'package:island/pods/chat/chat_summary.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
@@ -28,6 +27,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
|
|
||||||
part 'chat.g.dart';
|
part 'chat.g.dart';
|
||||||
|
|
||||||
@@ -289,7 +289,6 @@ class ChatListBodyWidget extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final chats = ref.watch(chatroomsJoinedProvider);
|
final chats = ref.watch(chatroomsJoinedProvider);
|
||||||
final callState = ref.watch(callNotifierProvider);
|
|
||||||
|
|
||||||
Widget bodyWidget = Column(
|
Widget bodyWidget = Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -314,10 +313,8 @@ class ChatListBodyWidget extends HookConsumerWidget {
|
|||||||
() => Future.sync(() {
|
() => Future.sync(() {
|
||||||
ref.invalidate(chatroomsJoinedProvider);
|
ref.invalidate(chatroomsJoinedProvider);
|
||||||
}),
|
}),
|
||||||
child: ListView.builder(
|
child: SuperListView.builder(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(bottom: 96),
|
||||||
bottom: callState.isConnected ? 96 : 0,
|
|
||||||
),
|
|
||||||
itemCount:
|
itemCount:
|
||||||
items
|
items
|
||||||
.where(
|
.where(
|
||||||
|
|||||||
@@ -487,6 +487,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteChatRoomHint'.tr(),
|
'deleteChatRoomHint'.tr(),
|
||||||
'deleteChatRoom'.tr(),
|
'deleteChatRoom'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) async {
|
).then((confirm) async {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
|
|||||||
@@ -304,16 +304,18 @@ class CreatorHubScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void deletePublisher() {
|
void deletePublisher() {
|
||||||
showConfirmAlert('deletePublisherHint'.tr(), 'deletePublisher'.tr()).then(
|
showConfirmAlert(
|
||||||
(confirm) {
|
'deletePublisherHint'.tr(),
|
||||||
if (confirm) {
|
'deletePublisher'.tr(),
|
||||||
final client = ref.watch(apiClientProvider);
|
isDanger: true,
|
||||||
client.delete('/sphere/publishers/${currentPublisher.value!.name}');
|
).then((confirm) {
|
||||||
ref.invalidate(publishersManagedProvider);
|
if (confirm) {
|
||||||
currentPublisher.value = null;
|
final client = ref.watch(apiClientProvider);
|
||||||
}
|
client.delete('/sphere/publishers/${currentPublisher.value!.name}');
|
||||||
},
|
ref.invalidate(publishersManagedProvider);
|
||||||
);
|
currentPublisher.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<DropdownMenuItem<SnPublisher>> publishersMenu = publishers.when(
|
final List<DropdownMenuItem<SnPublisher>> publishersMenu = publishers.when(
|
||||||
|
|||||||
@@ -8,18 +8,16 @@ import 'package:island/pods/site_pages.dart';
|
|||||||
import 'package:island/widgets/sites/page_form.dart';
|
import 'package:island/widgets/sites/page_form.dart';
|
||||||
import 'package:island/widgets/sites/site_action_menu.dart';
|
import 'package:island/widgets/sites/site_action_menu.dart';
|
||||||
import 'package:island/widgets/sites/site_detail_content.dart';
|
import 'package:island/widgets/sites/site_detail_content.dart';
|
||||||
|
import 'package:island/widgets/sites/site_info_card.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:island/widgets/sites/info_row.dart';
|
|
||||||
import 'package:island/widgets/sites/pages_section.dart';
|
import 'package:island/widgets/sites/pages_section.dart';
|
||||||
import 'package:island/widgets/sites/file_management_section.dart';
|
import 'package:island/widgets/sites/file_management_section.dart';
|
||||||
import 'package:island/widgets/sites/file_management_action_section.dart';
|
import 'package:island/widgets/sites/file_management_action_section.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/services/time.dart';
|
|
||||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
|
|
||||||
part 'site_detail.g.dart';
|
part 'site_detail.g.dart';
|
||||||
|
|
||||||
@@ -67,7 +65,6 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
body: siteAsync.when(
|
body: siteAsync.when(
|
||||||
data: (site) {
|
data: (site) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
if (isWideScreen(context)) {
|
if (isWideScreen(context)) {
|
||||||
return ExtendedRefreshIndicator(
|
return ExtendedRefreshIndicator(
|
||||||
onRefresh:
|
onRefresh:
|
||||||
@@ -99,76 +96,7 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Card(
|
SiteInfoCard(site: site),
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'siteInformation'.tr(),
|
|
||||||
style: theme.textTheme.titleMedium
|
|
||||||
?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
InfoRow(
|
|
||||||
label: 'name'.tr(),
|
|
||||||
value: site.name,
|
|
||||||
icon: Symbols.title,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
InfoRow(
|
|
||||||
label: 'slug'.tr(),
|
|
||||||
value: site.slug,
|
|
||||||
icon: Symbols.tag,
|
|
||||||
monospace: true,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
InfoRow(
|
|
||||||
label: 'siteDomain'.tr(),
|
|
||||||
value: '${site.slug}.solian.page',
|
|
||||||
icon: Symbols.globe,
|
|
||||||
monospace: true,
|
|
||||||
onTap: () {
|
|
||||||
final url =
|
|
||||||
'https://${site.slug}.solian.page';
|
|
||||||
launchUrlString(url);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
InfoRow(
|
|
||||||
label: 'siteMode'.tr(),
|
|
||||||
value:
|
|
||||||
site.mode == 0
|
|
||||||
? 'siteModeFullyManaged'.tr()
|
|
||||||
: 'siteModeSelfManaged'.tr(),
|
|
||||||
icon: Symbols.settings,
|
|
||||||
),
|
|
||||||
if (site.description != null &&
|
|
||||||
site.description!.isNotEmpty) ...[
|
|
||||||
const Gap(8),
|
|
||||||
InfoRow(
|
|
||||||
label: 'description'.tr(),
|
|
||||||
value: site.description!,
|
|
||||||
icon: Symbols.description,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const Gap(8),
|
|
||||||
InfoRow(
|
|
||||||
label: 'siteCreated'.tr(),
|
|
||||||
value: site.createdAt.formatSystem(),
|
|
||||||
icon: Symbols.calendar_add_on,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
InfoRow(
|
|
||||||
label: 'siteUpdated'.tr(),
|
|
||||||
value: site.updatedAt.formatSystem(),
|
|
||||||
icon: Symbols.update,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (site.mode == 1) // Self-Managed only
|
if (site.mode == 1) // Self-Managed only
|
||||||
FileManagementActionSection(
|
FileManagementActionSection(
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ class SiteForm extends HookConsumerWidget {
|
|||||||
final confirmed = await showConfirmAlert(
|
final confirmed = await showConfirmAlert(
|
||||||
'publicationSiteDeleteConfirm'.tr(),
|
'publicationSiteDeleteConfirm'.tr(),
|
||||||
'deletePublicationSite'.tr(),
|
'deletePublicationSite'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (confirmed != true) return;
|
if (confirmed != true) return;
|
||||||
|
|
||||||
|
|||||||
@@ -221,7 +221,9 @@ class _CreatorSiteItem extends HookConsumerWidget {
|
|||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
try {
|
try {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
await client.delete('/zone/sites/${site.id}');
|
await client.delete(
|
||||||
|
'/zone/sites/$pubName/${site.slug}',
|
||||||
|
);
|
||||||
ref.invalidate(siteListNotifierProvider(pubName));
|
ref.invalidate(siteListNotifierProvider(pubName));
|
||||||
showSnackBar('siteDeletedSuccess'.tr());
|
showSnackBar('siteDeletedSuccess'.tr());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ class StickerPackActionMenu extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteStickerPackHint'.tr(),
|
'deleteStickerPackHint'.tr(),
|
||||||
'deleteStickerPack'.tr(),
|
'deleteStickerPack'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class WebfeedForm extends HookConsumerWidget {
|
|||||||
final confirmed = await showConfirmAlert(
|
final confirmed = await showConfirmAlert(
|
||||||
'Are you sure you want to delete this web feed? This action cannot be undone.',
|
'Are you sure you want to delete this web feed? This action cannot be undone.',
|
||||||
'Delete Web Feed',
|
'Delete Web Feed',
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (confirmed != true) return;
|
if (confirmed != true) return;
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ class AppSecretsScreen extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteSecretHint'.tr(),
|
'deleteSecretHint'.tr(),
|
||||||
'deleteSecret'.tr(),
|
'deleteSecret'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ class CustomAppsScreen extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteCustomAppHint'.tr(),
|
'deleteCustomAppHint'.tr(),
|
||||||
'deleteCustomApp'.tr(),
|
'deleteCustomApp'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.read(
|
final client = ref.read(
|
||||||
|
|||||||
@@ -159,9 +159,11 @@ class BotKeysScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void revokeKey(String keyId) {
|
void revokeKey(String keyId) {
|
||||||
showConfirmAlert('revokeBotKeyHint'.tr(), 'revokeBotKey'.tr()).then((
|
showConfirmAlert(
|
||||||
confirm,
|
'revokeBotKeyHint'.tr(),
|
||||||
) {
|
'revokeBotKey'.tr(),
|
||||||
|
isDanger: true,
|
||||||
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
client
|
client
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ class BotsScreen extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteBotHint'.tr(),
|
'deleteBotHint'.tr(),
|
||||||
'deleteBot'.tr(),
|
'deleteBot'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
|
|||||||
@@ -631,6 +631,7 @@ class _ProjectListTile extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteProjectHint'.tr(),
|
'deleteProjectHint'.tr(),
|
||||||
'deleteProject'.tr(),
|
'deleteProject'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -30,7 +31,9 @@ import 'package:island/widgets/publisher/publisher_card.dart';
|
|||||||
import 'package:island/widgets/web_article_card.dart';
|
import 'package:island/widgets/web_article_card.dart';
|
||||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
import 'package:island/services/event_bus.dart';
|
import 'package:island/services/event_bus.dart';
|
||||||
|
import 'package:island/widgets/share/share_sheet.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
|
|
||||||
part 'explore.g.dart';
|
part 'explore.g.dart';
|
||||||
|
|
||||||
@@ -239,23 +242,74 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final appBar = isWide ? null : _buildAppBar(tabController, context);
|
final appBar = isWide ? null : _buildAppBar(tabController, context);
|
||||||
|
|
||||||
return AppScaffold(
|
final dragging = useState(false);
|
||||||
isNoBackground: false,
|
|
||||||
appBar: appBar,
|
return DropTarget(
|
||||||
body:
|
onDragDone: (detail) {
|
||||||
isWide
|
dragging.value = false;
|
||||||
? _buildWideBody(
|
if (detail.files.isNotEmpty) {
|
||||||
context,
|
showModalBottomSheet(
|
||||||
ref,
|
context: context,
|
||||||
filterBar,
|
isScrollControlled: true,
|
||||||
user,
|
useRootNavigator: true,
|
||||||
notificationCount,
|
builder: (context) => ShareSheet.files(files: detail.files),
|
||||||
query,
|
);
|
||||||
events,
|
}
|
||||||
selectedDay,
|
},
|
||||||
currentFilter.value,
|
onDragEntered: (_) => dragging.value = true,
|
||||||
)
|
onDragExited: (_) => dragging.value = false,
|
||||||
: _buildNarrowBody(context, ref, currentFilter.value),
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
|
appBar: appBar,
|
||||||
|
body:
|
||||||
|
isWide
|
||||||
|
? _buildWideBody(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
filterBar,
|
||||||
|
user,
|
||||||
|
notificationCount,
|
||||||
|
query,
|
||||||
|
events,
|
||||||
|
selectedDay,
|
||||||
|
currentFilter.value,
|
||||||
|
)
|
||||||
|
: _buildNarrowBody(context, ref, currentFilter.value),
|
||||||
|
),
|
||||||
|
if (dragging.value)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primaryContainer.withOpacity(0.9),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.upload_file,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(
|
||||||
|
'dropToShare'.tr(),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.headlineMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,7 +636,7 @@ class _DiscoveryActivityItem extends StatelessWidget {
|
|||||||
final height = type == 'post' ? 280.0 : 180.0;
|
final height = type == 'post' ? 280.0 : 180.0;
|
||||||
|
|
||||||
final contentWidget = switch (type) {
|
final contentWidget = switch (type) {
|
||||||
'post' => ListView.separated(
|
'post' => SuperListView.separated(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: items.length,
|
itemCount: items.length,
|
||||||
separatorBuilder: (context, index) => const Gap(12),
|
separatorBuilder: (context, index) => const Gap(12),
|
||||||
|
|||||||
@@ -256,21 +256,25 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ComposeFormFields(
|
Expanded(
|
||||||
state: state,
|
child: SingleChildScrollView(
|
||||||
showPublisherAvatar: false,
|
child: ComposeFormFields(
|
||||||
onPublisherTap: () {
|
state: state,
|
||||||
showModalBottomSheet(
|
showPublisherAvatar: false,
|
||||||
isScrollControlled: true,
|
onPublisherTap: () {
|
||||||
context: context,
|
showModalBottomSheet(
|
||||||
builder: (context) => const PublisherModal(),
|
isScrollControlled: true,
|
||||||
).then((value) {
|
context: context,
|
||||||
if (value != null) {
|
builder: (context) => const PublisherModal(),
|
||||||
state.currentPublisher.value = value;
|
).then((value) {
|
||||||
}
|
if (value != null) {
|
||||||
});
|
state.currentPublisher.value = value;
|
||||||
},
|
}
|
||||||
).padding(top: 16),
|
});
|
||||||
|
},
|
||||||
|
).padding(top: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Attachments preview
|
// Attachments preview
|
||||||
ValueListenableBuilder<List<UniversalFile>>(
|
ValueListenableBuilder<List<UniversalFile>>(
|
||||||
|
|||||||
@@ -145,9 +145,11 @@ class PostActionButtons extends HookConsumerWidget {
|
|||||||
message: 'delete'.tr(),
|
message: 'delete'.tr(),
|
||||||
child: FilledButton.tonal(
|
child: FilledButton.tonal(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showConfirmAlert('deletePostHint'.tr(), 'deletePost'.tr()).then((
|
showConfirmAlert(
|
||||||
confirm,
|
'deletePostHint'.tr(),
|
||||||
) {
|
'deletePost'.tr(),
|
||||||
|
isDanger: true,
|
||||||
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
client
|
client
|
||||||
|
|||||||
@@ -427,6 +427,7 @@ class _RealmActionMenu extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteRealmHint'.tr(),
|
'deleteRealmHint'.tr(),
|
||||||
'deleteRealm'.tr(),
|
'deleteRealm'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ class AccountSessionSheet extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'authDeviceLogoutHint'.tr(),
|
'authDeviceLogoutHint'.tr(),
|
||||||
'authDeviceLogout'.tr(),
|
'authDeviceLogout'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirm || !context.mounted) return;
|
if (!confirm || !context.mounted) return;
|
||||||
try {
|
try {
|
||||||
@@ -276,6 +277,7 @@ class AccountSessionSheet extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'authDeviceLogoutHint'.tr(),
|
'authDeviceLogoutHint'.tr(),
|
||||||
'authDeviceLogout'.tr(),
|
'authDeviceLogout'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (confirm && context.mounted) {
|
if (confirm && context.mounted) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:island/main.dart';
|
import 'package:island/main.dart';
|
||||||
import 'package:island/talker.dart';
|
import 'package:island/talker.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
||||||
|
|
||||||
@@ -156,6 +157,9 @@ String _parseRemoteError(DioException err) {
|
|||||||
return message ?? err.toString();
|
return message ?? err.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track active overlay dialogs for dismissal
|
||||||
|
final List<void Function()> _activeOverlayDialogs = [];
|
||||||
|
|
||||||
Future<T?> showOverlayDialog<T>({
|
Future<T?> showOverlayDialog<T>({
|
||||||
required Widget Function(BuildContext context, void Function(T? result) close)
|
required Widget Function(BuildContext context, void Function(T? result) close)
|
||||||
builder,
|
builder,
|
||||||
@@ -174,6 +178,7 @@ Future<T?> showOverlayDialog<T>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry.remove();
|
entry.remove();
|
||||||
|
_activeOverlayDialogs.remove(close);
|
||||||
completer.complete(result);
|
completer.complete(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,11 +219,24 @@ Future<T?> showOverlayDialog<T>({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_activeOverlayDialogs.add(() => close(null));
|
||||||
globalOverlay.currentState!.insert(entry);
|
globalOverlay.currentState!.insert(entry);
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
void showErrorAlert(dynamic err) {
|
// Close the topmost overlay dialog if any exists
|
||||||
|
bool closeTopmostOverlayDialog() {
|
||||||
|
if (_activeOverlayDialogs.isNotEmpty) {
|
||||||
|
final closeFunc = _activeOverlayDialogs.last;
|
||||||
|
closeFunc();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kDialogMaxWidth = 480.0;
|
||||||
|
|
||||||
|
void showErrorAlert(dynamic err, {IconData? icon}) {
|
||||||
if (err is Error) {
|
if (err is Error) {
|
||||||
talker.error('Something went wrong...', err, err.stackTrace);
|
talker.error('Something went wrong...', err, err.stackTrace);
|
||||||
}
|
}
|
||||||
@@ -231,51 +249,128 @@ void showErrorAlert(dynamic err) {
|
|||||||
|
|
||||||
showOverlayDialog<void>(
|
showOverlayDialog<void>(
|
||||||
builder:
|
builder:
|
||||||
(context, close) => AlertDialog(
|
(context, close) => ConstrainedBox(
|
||||||
title: Text('somethingWentWrong'.tr()),
|
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
|
||||||
content: Text(text),
|
child: AlertDialog(
|
||||||
actions: [
|
title: null,
|
||||||
TextButton(
|
titlePadding: EdgeInsets.zero,
|
||||||
onPressed: () => close(null),
|
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||||
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon ?? Icons.error_outline_rounded,
|
||||||
|
size: 48,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(
|
||||||
|
'somethingWentWrong'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(text),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(null),
|
||||||
|
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showInfoAlert(String message, String title) {
|
void showInfoAlert(String message, String title, {IconData? icon}) {
|
||||||
showOverlayDialog<void>(
|
showOverlayDialog<void>(
|
||||||
builder:
|
builder:
|
||||||
(context, close) => AlertDialog(
|
(context, close) => ConstrainedBox(
|
||||||
title: Text(title),
|
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
|
||||||
content: Text(message),
|
child: AlertDialog(
|
||||||
actions: [
|
title: null,
|
||||||
TextButton(
|
titlePadding: EdgeInsets.zero,
|
||||||
onPressed: () => close(null),
|
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||||
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon ?? Symbols.info_rounded,
|
||||||
|
fill: 1,
|
||||||
|
size: 48,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(title, style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const Gap(8),
|
||||||
|
Text(message),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(null),
|
||||||
|
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> showConfirmAlert(String message, String title) async {
|
Future<bool> showConfirmAlert(
|
||||||
|
String message,
|
||||||
|
String title, {
|
||||||
|
IconData? icon,
|
||||||
|
bool isDanger = false,
|
||||||
|
}) async {
|
||||||
final result = await showOverlayDialog<bool>(
|
final result = await showOverlayDialog<bool>(
|
||||||
builder:
|
builder:
|
||||||
(context, close) => AlertDialog(
|
(context, close) => ConstrainedBox(
|
||||||
title: Text(title),
|
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
|
||||||
content: Text(message),
|
child: AlertDialog(
|
||||||
actions: [
|
title: null,
|
||||||
TextButton(
|
titlePadding: EdgeInsets.zero,
|
||||||
onPressed: () => close(false),
|
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon ?? Symbols.help_rounded,
|
||||||
|
size: 48,
|
||||||
|
fill: 1,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(title, style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const Gap(8),
|
||||||
|
Text(message),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
TextButton(
|
actions: [
|
||||||
onPressed: () => close(true),
|
TextButton(
|
||||||
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
onPressed: () => close(false),
|
||||||
),
|
child: Text(
|
||||||
],
|
MaterialLocalizations.of(context).cancelButtonLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(true),
|
||||||
|
style:
|
||||||
|
isDanger
|
||||||
|
? TextButton.styleFrom(
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return result ?? false;
|
return result ?? false;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:island/route.dart';
|
|||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/upload_overlay.dart';
|
import 'package:island/widgets/upload_overlay.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -364,6 +365,12 @@ class PopAction extends Action<PopIntent> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(PopIntent intent) {
|
void invoke(PopIntent intent) {
|
||||||
|
// First, try to close any overlay dialogs
|
||||||
|
if (closeTopmostOverlayDialog()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no overlay to close, pop the route
|
||||||
if (ref.watch(routerProvider).canPop()) {
|
if (ref.watch(routerProvider).canPop()) {
|
||||||
ref.read(routerProvider).pop();
|
ref.read(routerProvider).pop();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -596,6 +596,7 @@ class MessageHoverActionMenu extends StatelessWidget {
|
|||||||
final confirmed = await showConfirmAlert(
|
final confirmed = await showConfirmAlert(
|
||||||
'deleteMessageConfirmation'.tr(),
|
'deleteMessageConfirmation'.tr(),
|
||||||
'deleteMessage'.tr(),
|
'deleteMessage'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class NetworkStatusSheet extends HookConsumerWidget {
|
|||||||
final wsState = ref.watch(websocketStateProvider);
|
final wsState = ref.watch(websocketStateProvider);
|
||||||
|
|
||||||
return SheetScaffold(
|
return SheetScaffold(
|
||||||
|
heightFactor: 0.4,
|
||||||
titleText:
|
titleText:
|
||||||
wsState == WebSocketState.connected()
|
wsState == WebSocketState.connected()
|
||||||
? 'Connection Status'
|
? 'Connection Status'
|
||||||
|
|||||||
@@ -512,6 +512,7 @@ class FileListView extends HookConsumerWidget {
|
|||||||
final confirmed = await showConfirmAlert(
|
final confirmed = await showConfirmAlert(
|
||||||
'Are you sure you want to delete the selected files?',
|
'Are you sure you want to delete the selected files?',
|
||||||
'Delete Selected Files',
|
'Delete Selected Files',
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@@ -742,22 +743,25 @@ class FileListView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Row(
|
SingleChildScrollView(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
scrollDirection: Axis.horizontal,
|
||||||
children: [
|
child: Row(
|
||||||
ElevatedButton.icon(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
onPressed: onPickAndUpload,
|
children: [
|
||||||
icon: const Icon(Symbols.upload_file),
|
ElevatedButton.icon(
|
||||||
label: const Text('Upload Files'),
|
onPressed: onPickAndUpload,
|
||||||
),
|
icon: const Icon(Symbols.upload_file),
|
||||||
const Gap(12),
|
label: const Text('Upload Files'),
|
||||||
OutlinedButton.icon(
|
),
|
||||||
onPressed:
|
const Gap(12),
|
||||||
() => onShowCreateDirectory(ref.context, currentPath),
|
OutlinedButton.icon(
|
||||||
icon: const Icon(Symbols.create_new_folder),
|
onPressed:
|
||||||
label: const Text('Create Directory'),
|
() => onShowCreateDirectory(ref.context, currentPath),
|
||||||
),
|
icon: const Icon(Symbols.create_new_folder),
|
||||||
],
|
label: const Text('Create Directory'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -785,6 +789,7 @@ class FileListView extends HookConsumerWidget {
|
|||||||
final confirmed = await showConfirmAlert(
|
final confirmed = await showConfirmAlert(
|
||||||
'confirmDeleteFile'.tr(),
|
'confirmDeleteFile'.tr(),
|
||||||
'deleteFile'.tr(),
|
'deleteFile'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
@@ -1153,6 +1158,7 @@ class FileListView extends HookConsumerWidget {
|
|||||||
final confirmed = await showConfirmAlert(
|
final confirmed = await showConfirmAlert(
|
||||||
'confirmDeleteFile'.tr(),
|
'confirmDeleteFile'.tr(),
|
||||||
'deleteFile'.tr(),
|
'deleteFile'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
@@ -1221,6 +1227,7 @@ class FileListView extends HookConsumerWidget {
|
|||||||
final confirmed = await showConfirmAlert(
|
final confirmed = await showConfirmAlert(
|
||||||
'confirmDeleteFile'.tr(),
|
'confirmDeleteFile'.tr(),
|
||||||
'deleteFile'.tr(),
|
'deleteFile'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
@@ -1263,6 +1270,7 @@ class FileListView extends HookConsumerWidget {
|
|||||||
final confirmed = await showConfirmAlert(
|
final confirmed = await showConfirmAlert(
|
||||||
'confirmDeleteFile'.tr(),
|
'confirmDeleteFile'.tr(),
|
||||||
'deleteFile'.tr(),
|
'deleteFile'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
import 'package:island/screens/posts/compose.dart';
|
import 'package:island/screens/posts/compose.dart';
|
||||||
|
import 'package:island/screens/posts/post_detail.dart';
|
||||||
import 'package:island/services/compose_storage_db.dart';
|
import 'package:island/services/compose_storage_db.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:island/widgets/post/compose_card.dart';
|
import 'package:island/widgets/post/compose_card.dart';
|
||||||
@@ -50,19 +51,33 @@ class PostComposeSheet extends HookConsumerWidget {
|
|||||||
final restoredInitialState = useState<PostComposeInitialState?>(null);
|
final restoredInitialState = useState<PostComposeInitialState?>(null);
|
||||||
final prompted = useState(false);
|
final prompted = useState(false);
|
||||||
|
|
||||||
final repliedPost = initialState?.replyingTo ?? originalPost?.repliedPost;
|
// Fetch full post data if we're editing a post
|
||||||
|
final fullPostData =
|
||||||
|
originalPost != null
|
||||||
|
? ref.watch(postProvider(originalPost!.id))
|
||||||
|
: const AsyncValue.data(null);
|
||||||
|
|
||||||
|
// Use the full post data if available, otherwise fall back to originalPost
|
||||||
|
final effectiveOriginalPost = fullPostData.when(
|
||||||
|
data: (fullPost) => fullPost ?? originalPost,
|
||||||
|
loading: () => originalPost,
|
||||||
|
error: (_, _) => originalPost,
|
||||||
|
);
|
||||||
|
|
||||||
|
final repliedPost =
|
||||||
|
initialState?.replyingTo ?? effectiveOriginalPost?.repliedPost;
|
||||||
final forwardedPost =
|
final forwardedPost =
|
||||||
initialState?.forwardingTo ?? originalPost?.forwardedPost;
|
initialState?.forwardingTo ?? effectiveOriginalPost?.forwardedPost;
|
||||||
|
|
||||||
// Create compose state
|
// Create compose state
|
||||||
final ComposeState state = useMemoized(
|
final ComposeState state = useMemoized(
|
||||||
() => ComposeLogic.createState(
|
() => ComposeLogic.createState(
|
||||||
originalPost: originalPost,
|
originalPost: effectiveOriginalPost,
|
||||||
forwardedPost: forwardedPost,
|
forwardedPost: forwardedPost,
|
||||||
repliedPost: repliedPost,
|
repliedPost: repliedPost,
|
||||||
postType: 0,
|
postType: 0,
|
||||||
),
|
),
|
||||||
[originalPost, forwardedPost, repliedPost],
|
[effectiveOriginalPost, forwardedPost, repliedPost],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add a listener to the entire state to trigger rebuilds
|
// Add a listener to the entire state to trigger rebuilds
|
||||||
@@ -112,7 +127,7 @@ class PostComposeSheet extends HookConsumerWidget {
|
|||||||
ref,
|
ref,
|
||||||
state,
|
state,
|
||||||
context,
|
context,
|
||||||
originalPost: originalPost,
|
originalPost: effectiveOriginalPost,
|
||||||
repliedPost: repliedPost,
|
repliedPost: repliedPost,
|
||||||
forwardedPost: forwardedPost,
|
forwardedPost: forwardedPost,
|
||||||
onSuccess: () {
|
onSuccess: () {
|
||||||
@@ -139,8 +154,13 @@ class PostComposeSheet extends HookConsumerWidget {
|
|||||||
height: 24,
|
height: 24,
|
||||||
child: const CircularProgressIndicator(strokeWidth: 2),
|
child: const CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: Icon(originalPost != null ? Symbols.edit : Symbols.upload),
|
: Icon(
|
||||||
tooltip: originalPost != null ? 'postUpdate'.tr() : 'postPublish'.tr(),
|
effectiveOriginalPost != null ? Symbols.edit : Symbols.upload,
|
||||||
|
),
|
||||||
|
tooltip:
|
||||||
|
effectiveOriginalPost != null
|
||||||
|
? 'postUpdate'.tr()
|
||||||
|
: 'postPublish'.tr(),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -148,7 +168,7 @@ class PostComposeSheet extends HookConsumerWidget {
|
|||||||
titleText: 'postCompose'.tr(),
|
titleText: 'postCompose'.tr(),
|
||||||
actions: actions,
|
actions: actions,
|
||||||
child: PostComposeCard(
|
child: PostComposeCard(
|
||||||
originalPost: originalPost,
|
originalPost: effectiveOriginalPost,
|
||||||
initialState: restoredInitialState.value ?? initialState,
|
initialState: restoredInitialState.value ?? initialState,
|
||||||
onCancel: () => Navigator.of(context).pop(),
|
onCancel: () => Navigator.of(context).pop(),
|
||||||
onSubmit: () {
|
onSubmit: () {
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ class DraftManagerSheet extends HookConsumerWidget {
|
|||||||
final confirmed = await showConfirmAlert(
|
final confirmed = await showConfirmAlert(
|
||||||
'clearAllDraftsConfirm'.tr(),
|
'clearAllDraftsConfirm'.tr(),
|
||||||
'clearAllDrafts'.tr(),
|
'clearAllDrafts'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ class PostActionableItem extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deletePostHint'.tr(),
|
'deletePostHint'.tr(),
|
||||||
'deletePost'.tr(),
|
'deletePost'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
|
|||||||
@@ -69,22 +69,24 @@ class PostItemCreator extends HookConsumerWidget {
|
|||||||
title: 'delete'.tr(),
|
title: 'delete'.tr(),
|
||||||
image: MenuImage.icon(Symbols.delete),
|
image: MenuImage.icon(Symbols.delete),
|
||||||
callback: () {
|
callback: () {
|
||||||
showConfirmAlert('deletePostHint'.tr(), 'deletePost'.tr()).then(
|
showConfirmAlert(
|
||||||
(confirm) {
|
'deletePostHint'.tr(),
|
||||||
if (confirm) {
|
'deletePost'.tr(),
|
||||||
final client = ref.watch(apiClientProvider);
|
isDanger: true,
|
||||||
client
|
).then((confirm) {
|
||||||
.delete('/sphere/posts/${item.id}')
|
if (confirm) {
|
||||||
.catchError((err) {
|
final client = ref.watch(apiClientProvider);
|
||||||
showErrorAlert(err);
|
client
|
||||||
return err;
|
.delete('/sphere/posts/${item.id}')
|
||||||
})
|
.catchError((err) {
|
||||||
.then((_) {
|
showErrorAlert(err);
|
||||||
onRefresh?.call();
|
return err;
|
||||||
});
|
})
|
||||||
}
|
.then((_) {
|
||||||
},
|
onRefresh?.call();
|
||||||
);
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
MenuSeparator(),
|
MenuSeparator(),
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ class PostReplyPreview extends HookConsumerWidget {
|
|||||||
: (featuredReply!).map(
|
: (featuredReply!).map(
|
||||||
data:
|
data:
|
||||||
(data) => Row(
|
(data) => Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
ProfilePictureWidget(
|
ProfilePictureWidget(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/services/file_uploader.dart';
|
import 'package:island/services/file_uploader.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
@@ -177,8 +178,11 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
|
|||||||
|
|
||||||
// Show compose sheet
|
// Show compose sheet
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
PostComposeSheet.show(context, initialState: initialState);
|
await PostComposeSheet.show(context, initialState: initialState);
|
||||||
Navigator.of(context).pop(); // Close the share sheet
|
// Close the share sheet after the compose sheet is dismissed
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
@@ -281,23 +285,10 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Show navigation prompt
|
// Show navigation prompt
|
||||||
final shouldNavigate = await showDialog<bool>(
|
final shouldNavigate = await showConfirmAlert(
|
||||||
context: context,
|
'wouldYouLikeToGoToChat'.tr(),
|
||||||
builder:
|
'shareSuccess'.tr(),
|
||||||
(context) => AlertDialog(
|
icon: Symbols.check_circle,
|
||||||
title: Text('shareSuccess'.tr()),
|
|
||||||
content: Text('wouldYouLikeToGoToChat'.tr()),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
|
||||||
child: Text('no'.tr()),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
child: Text('yes'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Close the share sheet
|
// Close the share sheet
|
||||||
@@ -363,6 +354,92 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _uploadFiles() async {
|
||||||
|
if (widget.content.files == null || widget.content.files!.isEmpty) return;
|
||||||
|
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
try {
|
||||||
|
final universalFiles =
|
||||||
|
widget.content.files!.map((file) {
|
||||||
|
UniversalFileType fileType;
|
||||||
|
if (file.mimeType?.startsWith('image/') == true) {
|
||||||
|
fileType = UniversalFileType.image;
|
||||||
|
} else if (file.mimeType?.startsWith('video/') == true) {
|
||||||
|
fileType = UniversalFileType.video;
|
||||||
|
} else if (file.mimeType?.startsWith('audio/') == true) {
|
||||||
|
fileType = UniversalFileType.audio;
|
||||||
|
} else {
|
||||||
|
fileType = UniversalFileType.file;
|
||||||
|
}
|
||||||
|
return UniversalFile(data: file, type: fileType);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Initialize progress tracking
|
||||||
|
final messageId = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
_fileUploadProgress[messageId] = List.filled(universalFiles.length, 0.0);
|
||||||
|
|
||||||
|
List<SnCloudFile> uploadedFiles = [];
|
||||||
|
|
||||||
|
// Upload each file
|
||||||
|
for (var idx = 0; idx < universalFiles.length; idx++) {
|
||||||
|
final file = universalFiles[idx];
|
||||||
|
final cloudFile =
|
||||||
|
await FileUploader.createCloudFile(
|
||||||
|
ref: ref,
|
||||||
|
fileData: file,
|
||||||
|
onProgress: (progress, _) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_fileUploadProgress[messageId]?[idx] = progress ?? 0.0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).future;
|
||||||
|
|
||||||
|
if (cloudFile == null) {
|
||||||
|
throw Exception('Failed to upload file: ${file.data.name}');
|
||||||
|
}
|
||||||
|
uploadedFiles.add(cloudFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
// Show success message
|
||||||
|
showSnackBar('uploadSuccess'.tr());
|
||||||
|
|
||||||
|
// If single file, ask to view details
|
||||||
|
if (uploadedFiles.length == 1) {
|
||||||
|
final shouldView = await showConfirmAlert(
|
||||||
|
'wouldYouLikeToViewFile'.tr(),
|
||||||
|
'uploadSuccess'.tr(),
|
||||||
|
icon: Symbols.check_circle,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop(); // Close share sheet
|
||||||
|
if (shouldView == true) {
|
||||||
|
context.pushNamed(
|
||||||
|
'fileDetail',
|
||||||
|
pathParameters: {'id': uploadedFiles.first.id},
|
||||||
|
extra: uploadedFiles.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Just close for multiple files
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _copyToClipboard() async {
|
Future<void> _copyToClipboard() async {
|
||||||
try {
|
try {
|
||||||
String textToCopy = '';
|
String textToCopy = '';
|
||||||
@@ -452,6 +529,15 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
|
|||||||
onTap: _isLoading ? null : _shareToPost,
|
onTap: _isLoading ? null : _shareToPost,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
if (widget.content.type ==
|
||||||
|
ShareContentType.file) ...[
|
||||||
|
_CompactShareOption(
|
||||||
|
icon: Symbols.cloud_upload,
|
||||||
|
title: 'upload'.tr(),
|
||||||
|
onTap: _isLoading ? null : _uploadFiles,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
],
|
||||||
_CompactShareOption(
|
_CompactShareOption(
|
||||||
icon: Symbols.content_copy,
|
icon: Symbols.content_copy,
|
||||||
title: 'copy'.tr(),
|
title: 'copy'.tr(),
|
||||||
@@ -650,19 +736,26 @@ class _ChatRoomsList extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatRoomOption extends StatelessWidget {
|
class _ChatRoomOption extends HookConsumerWidget {
|
||||||
final SnChatRoom room;
|
final SnChatRoom room;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
const _ChatRoomOption({required this.room, this.onTap});
|
const _ChatRoomOption({required this.room, this.onTap});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final userInfo = ref.watch(userInfoProvider);
|
||||||
|
|
||||||
|
final validMembers =
|
||||||
|
(room.members ?? [])
|
||||||
|
.where((m) => m.accountId != userInfo.value?.id)
|
||||||
|
.toList();
|
||||||
|
|
||||||
final isDirect = room.type == 1; // Assuming type 1 is direct chat
|
final isDirect = room.type == 1; // Assuming type 1 is direct chat
|
||||||
final displayName =
|
final displayName =
|
||||||
room.name ??
|
room.name ??
|
||||||
(isDirect && room.members != null
|
(isDirect
|
||||||
? room.members!.map((m) => m.account.nick).join(', ')
|
? validMembers.map((m) => m.account.nick).join(', ')
|
||||||
: 'unknownChat'.tr());
|
: 'unknownChat'.tr());
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@@ -694,18 +787,22 @@ class _ChatRoomOption extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child:
|
child:
|
||||||
room.picture != null
|
(isDirect && room.picture?.id == null)
|
||||||
? ClipRRect(
|
? SplitAvatarWidget(
|
||||||
borderRadius: BorderRadius.circular(16),
|
filesId:
|
||||||
child: CloudFileWidget(
|
validMembers
|
||||||
item: room.picture!,
|
.map((e) => e.account.profile.picture?.id)
|
||||||
fit: BoxFit.cover,
|
.toList(),
|
||||||
),
|
radius: 16,
|
||||||
)
|
)
|
||||||
: Icon(
|
: room.picture?.id == null
|
||||||
isDirect ? Symbols.person : Symbols.group,
|
? CircleAvatar(
|
||||||
size: 20,
|
radius: 16,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
child: Text(room.name![0].toUpperCase()),
|
||||||
|
)
|
||||||
|
: ProfilePictureWidget(
|
||||||
|
fileId: room.picture?.id,
|
||||||
|
radius: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|||||||
@@ -72,26 +72,10 @@ class FileManagementActionSection extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _purgeFiles(BuildContext context, WidgetRef ref) async {
|
Future<void> _purgeFiles(BuildContext context, WidgetRef ref) async {
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showConfirmAlert(
|
||||||
context: context,
|
'purgeFilesConfirm'.tr(),
|
||||||
builder:
|
'confirmPurge'.tr(),
|
||||||
(context) => AlertDialog(
|
isDanger: true,
|
||||||
title: Text('confirmPurge'.tr()),
|
|
||||||
content: Text('purgeFilesConfirm'.tr()),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, false),
|
|
||||||
child: Text('cancel'.tr()),
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
child: Text('purgeAllFiles'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed != true) return;
|
if (confirmed != true) return;
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class SiteActionMenu extends HookConsumerWidget {
|
|||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
try {
|
try {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
await client.delete('/zone/sites/${site.id}');
|
await client.delete('/zone/sites/$pubName/${site.slug}');
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showSnackBar('siteDeletedSuccess'.tr());
|
showSnackBar('siteDeletedSuccess'.tr());
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/publication_site.dart';
|
import 'package:island/models/publication_site.dart';
|
||||||
import 'package:island/widgets/sites/file_management_section.dart';
|
import 'package:island/widgets/sites/file_management_section.dart';
|
||||||
import 'package:island/widgets/sites/file_management_action_section.dart';
|
import 'package:island/widgets/sites/file_management_action_section.dart';
|
||||||
import 'package:island/widgets/sites/info_row.dart';
|
import 'package:island/widgets/sites/site_info_card.dart';
|
||||||
import 'package:island/widgets/sites/pages_section.dart';
|
import 'package:island/widgets/sites/pages_section.dart';
|
||||||
import 'package:island/services/time.dart';
|
|
||||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
import 'package:island/screens/creators/sites/site_detail.dart';
|
import 'package:island/screens/creators/sites/site_detail.dart';
|
||||||
|
|
||||||
class SiteDetailContent extends HookConsumerWidget {
|
class SiteDetailContent extends HookConsumerWidget {
|
||||||
@@ -24,8 +21,6 @@ class SiteDetailContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return ExtendedRefreshIndicator(
|
return ExtendedRefreshIndicator(
|
||||||
onRefresh:
|
onRefresh:
|
||||||
() async =>
|
() async =>
|
||||||
@@ -36,65 +31,7 @@ class SiteDetailContent extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Site Info Card
|
// Site Info Card
|
||||||
Card(
|
SiteInfoCard(site: site),
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'siteInformation'.tr(),
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
InfoRow(
|
|
||||||
label: 'name'.tr(),
|
|
||||||
value: site.name,
|
|
||||||
icon: Symbols.title,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
InfoRow(
|
|
||||||
label: 'slug'.tr(),
|
|
||||||
value: site.slug,
|
|
||||||
icon: Symbols.tag,
|
|
||||||
monospace: true,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
InfoRow(
|
|
||||||
label: 'Mode',
|
|
||||||
value:
|
|
||||||
site.mode == 0
|
|
||||||
? 'siteModeFullyManaged'.tr()
|
|
||||||
: 'siteModeSelfManaged'.tr(),
|
|
||||||
icon: Symbols.settings,
|
|
||||||
),
|
|
||||||
if (site.description != null &&
|
|
||||||
site.description!.isNotEmpty) ...[
|
|
||||||
const Gap(8),
|
|
||||||
InfoRow(
|
|
||||||
label: 'description'.tr(),
|
|
||||||
value: site.description!,
|
|
||||||
icon: Symbols.description,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const Gap(8),
|
|
||||||
InfoRow(
|
|
||||||
label: 'siteCreated'.tr(),
|
|
||||||
value: site.createdAt.formatSystem(),
|
|
||||||
icon: Symbols.calendar_add_on,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
InfoRow(
|
|
||||||
label: 'siteUpdated'.tr(),
|
|
||||||
value: site.updatedAt.formatSystem(),
|
|
||||||
icon: Symbols.update,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (site.mode == 1) // Self-Managed only
|
if (site.mode == 1) // Self-Managed only
|
||||||
FileManagementActionSection(site: site, pubName: pubName),
|
FileManagementActionSection(site: site, pubName: pubName),
|
||||||
|
|||||||
85
lib/widgets/sites/site_info_card.dart
Normal file
85
lib/widgets/sites/site_info_card.dart
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/services/time.dart';
|
||||||
|
import 'package:island/widgets/sites/info_row.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
class SiteInfoCard extends StatelessWidget {
|
||||||
|
final SnPublicationSite site;
|
||||||
|
|
||||||
|
const SiteInfoCard({super.key, required this.site});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'siteInformation'.tr(),
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
InfoRow(label: 'name'.tr(), value: site.name, icon: Symbols.title),
|
||||||
|
const Gap(8),
|
||||||
|
InfoRow(
|
||||||
|
label: 'slug'.tr(),
|
||||||
|
value: site.slug,
|
||||||
|
icon: Symbols.tag,
|
||||||
|
monospace: true,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
InfoRow(
|
||||||
|
label: 'siteDomain'.tr(),
|
||||||
|
value: '${site.slug}.solian.page',
|
||||||
|
icon: Symbols.globe,
|
||||||
|
monospace: true,
|
||||||
|
onTap: () {
|
||||||
|
final url = 'https://${site.slug}.solian.page';
|
||||||
|
launchUrlString(url);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
InfoRow(
|
||||||
|
label: 'siteMode'.tr(),
|
||||||
|
value:
|
||||||
|
site.mode == 0
|
||||||
|
? 'siteModeFullyManaged'.tr()
|
||||||
|
: 'siteModeSelfManaged'.tr(),
|
||||||
|
icon: Symbols.settings,
|
||||||
|
),
|
||||||
|
if (site.description != null && site.description!.isNotEmpty) ...[
|
||||||
|
const Gap(8),
|
||||||
|
InfoRow(
|
||||||
|
label: 'description'.tr(),
|
||||||
|
value: site.description!,
|
||||||
|
icon: Symbols.description,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const Gap(8),
|
||||||
|
InfoRow(
|
||||||
|
label: 'siteCreated'.tr(),
|
||||||
|
value: site.createdAt.formatSystem(),
|
||||||
|
icon: Symbols.calendar_add_on,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
InfoRow(
|
||||||
|
label: 'siteUpdated'.tr(),
|
||||||
|
value: site.updatedAt.formatSystem(),
|
||||||
|
icon: Symbols.update,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 3.3.0+148
|
version: 3.4.0+149
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.2
|
sdk: ^3.7.2
|
||||||
|
|||||||
Reference in New Issue
Block a user