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",
|
||||
"uploading": "Uploading",
|
||||
"uploadingProgress": "Uploading {} of {}",
|
||||
"upload": "Upload",
|
||||
"uploadSuccess": "Upload successful!",
|
||||
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||
"uploadAll": "Upload All",
|
||||
"stickerCopyPlaceholder": "Copy Placeholder",
|
||||
"realmSelection": "Select a Realm",
|
||||
@@ -1110,7 +1113,6 @@
|
||||
"deleteRecycledFiles": "Delete Recycled Files",
|
||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||
"upload": "Upload",
|
||||
"updateAvailable": "Update available",
|
||||
"noChangelogProvided": "No changelog provided.",
|
||||
"useSecondarySourceForDownload": "Use secondary source for download",
|
||||
@@ -1471,5 +1473,6 @@
|
||||
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||
"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": "未知聊天",
|
||||
"addAdditionalMessage": "添加附加消息……",
|
||||
"uploadingFiles": "上传文件中……",
|
||||
"sharedSuccessfully": "分享成功!",
|
||||
"shareSuccess": "分享成功!",
|
||||
"shareToSpecificChatSuccess": "成功分享至 {}!",
|
||||
"wouldYouLikeToGoToChat": "是否前往该聊天?",
|
||||
"sharedSuccessfully": "分享成功",
|
||||
"shareSuccess": "分享成功",
|
||||
"shareToSpecificChatSuccess": "成功分享至 {}",
|
||||
"wouldYouLikeToGoToChat": "是否前往该聊天页面?",
|
||||
"no": "否",
|
||||
"yes": "是",
|
||||
"navigateToChat": "前往聊天",
|
||||
@@ -1092,4 +1092,4 @@
|
||||
"aiThought": "寻思",
|
||||
"aiThoughtTitle": "让 SN 酱寻思寻思",
|
||||
"thoughtUnpaidHint": "寻思因为有未支付的订单而被禁用"
|
||||
}
|
||||
}
|
||||
@@ -140,21 +140,29 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
guard !attachmentUrls.isEmpty else {
|
||||
print("Invalid URLs for attachments: \(attachmentUrls)")
|
||||
self.contentHandler?(content)
|
||||
return
|
||||
}
|
||||
|
||||
let targetSize = 512
|
||||
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 {
|
||||
guard let remoteUrl = URL(string: attachmentUrl) else {
|
||||
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 ? [
|
||||
.processor(scaleProcessor)
|
||||
] : nil) { [weak self] result in
|
||||
defer { dispatchGroup.leave() }
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
@@ -166,49 +174,34 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
do {
|
||||
// Write the image data to a temporary file for UNNotificationAttachment
|
||||
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 {
|
||||
print("Failed to write media to temporary file: \(error.localizedDescription)")
|
||||
self.contentHandler?(content)
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
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
|
||||
self.contentHandler?(content)
|
||||
dispatchGroup.notify(queue: .main) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
content.attachments = attachments
|
||||
self.contentHandler?(content)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private func createMessageIntent(with sender: INPerson, meta: [AnyHashable: Any], body: String) -> INSendMessageIntent {
|
||||
INSendMessageIntent(
|
||||
recipients: nil,
|
||||
|
||||
@@ -62,6 +62,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
|
||||
final confirm = await showConfirmAlert(
|
||||
'accountDeletionHint'.tr(),
|
||||
'accountDeletion'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
if (!confirm || !context.mounted) return;
|
||||
try {
|
||||
|
||||
@@ -26,6 +26,7 @@ class AuthFactorSheet extends HookConsumerWidget {
|
||||
final confirm = await showConfirmAlert(
|
||||
'authFactorDeleteHint'.tr(),
|
||||
'authFactorDelete'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
if (!confirm || !context.mounted) return;
|
||||
try {
|
||||
|
||||
@@ -82,6 +82,7 @@ class AccountConnectionSheet extends HookConsumerWidget {
|
||||
final confirm = await showConfirmAlert(
|
||||
'accountConnectionDeleteHint'.tr(),
|
||||
'accountConnectionDelete'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
if (!confirm || !context.mounted) return;
|
||||
try {
|
||||
@@ -332,6 +333,7 @@ class AccountConnectionsSheet extends HookConsumerWidget {
|
||||
final confirm = await showConfirmAlert(
|
||||
'accountConnectionDeleteHint'.tr(),
|
||||
'accountConnectionDelete'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
if (confirm && context.mounted) {
|
||||
try {
|
||||
|
||||
@@ -20,6 +20,7 @@ class ContactMethodSheet extends HookConsumerWidget {
|
||||
final confirm = await showConfirmAlert(
|
||||
'contactMethodDeleteHint'.tr(),
|
||||
'contactMethodDelete'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
if (!confirm || !context.mounted) return;
|
||||
try {
|
||||
|
||||
@@ -11,7 +11,6 @@ import 'package:island/models/chat.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/account.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/network.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:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||
|
||||
part 'chat.g.dart';
|
||||
|
||||
@@ -289,7 +289,6 @@ class ChatListBodyWidget extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final chats = ref.watch(chatroomsJoinedProvider);
|
||||
final callState = ref.watch(callNotifierProvider);
|
||||
|
||||
Widget bodyWidget = Column(
|
||||
children: [
|
||||
@@ -314,10 +313,8 @@ class ChatListBodyWidget extends HookConsumerWidget {
|
||||
() => Future.sync(() {
|
||||
ref.invalidate(chatroomsJoinedProvider);
|
||||
}),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: callState.isConnected ? 96 : 0,
|
||||
),
|
||||
child: SuperListView.builder(
|
||||
padding: EdgeInsets.only(bottom: 96),
|
||||
itemCount:
|
||||
items
|
||||
.where(
|
||||
|
||||
@@ -487,6 +487,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
|
||||
showConfirmAlert(
|
||||
'deleteChatRoomHint'.tr(),
|
||||
'deleteChatRoom'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) async {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
|
||||
@@ -304,16 +304,18 @@ class CreatorHubScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
void deletePublisher() {
|
||||
showConfirmAlert('deletePublisherHint'.tr(), 'deletePublisher'.tr()).then(
|
||||
(confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
client.delete('/sphere/publishers/${currentPublisher.value!.name}');
|
||||
ref.invalidate(publishersManagedProvider);
|
||||
currentPublisher.value = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
showConfirmAlert(
|
||||
'deletePublisherHint'.tr(),
|
||||
'deletePublisher'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
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(
|
||||
|
||||
@@ -8,18 +8,16 @@ import 'package:island/pods/site_pages.dart';
|
||||
import 'package:island/widgets/sites/page_form.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_info_card.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:material_symbols_icons/symbols.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/file_management_section.dart';
|
||||
import 'package:island/widgets/sites/file_management_action_section.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
part 'site_detail.g.dart';
|
||||
|
||||
@@ -67,7 +65,6 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
|
||||
),
|
||||
body: siteAsync.when(
|
||||
data: (site) {
|
||||
final theme = Theme.of(context);
|
||||
if (isWideScreen(context)) {
|
||||
return ExtendedRefreshIndicator(
|
||||
onRefresh:
|
||||
@@ -99,76 +96,7 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SiteInfoCard(site: site),
|
||||
const Gap(8),
|
||||
if (site.mode == 1) // Self-Managed only
|
||||
FileManagementActionSection(
|
||||
|
||||
@@ -190,6 +190,7 @@ class SiteForm extends HookConsumerWidget {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'publicationSiteDeleteConfirm'.tr(),
|
||||
'deletePublicationSite'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
|
||||
|
||||
@@ -221,7 +221,9 @@ class _CreatorSiteItem extends HookConsumerWidget {
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/zone/sites/${site.id}');
|
||||
await client.delete(
|
||||
'/zone/sites/$pubName/${site.slug}',
|
||||
);
|
||||
ref.invalidate(siteListNotifierProvider(pubName));
|
||||
showSnackBar('siteDeletedSuccess'.tr());
|
||||
} catch (e) {
|
||||
|
||||
@@ -288,6 +288,7 @@ class StickerPackActionMenu extends HookConsumerWidget {
|
||||
showConfirmAlert(
|
||||
'deleteStickerPackHint'.tr(),
|
||||
'deleteStickerPack'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
|
||||
@@ -70,6 +70,7 @@ class WebfeedForm extends HookConsumerWidget {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'Are you sure you want to delete this web feed? This action cannot be undone.',
|
||||
'Delete Web Feed',
|
||||
isDanger: true,
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
|
||||
|
||||
@@ -211,6 +211,7 @@ class AppSecretsScreen extends HookConsumerWidget {
|
||||
showConfirmAlert(
|
||||
'deleteSecretHint'.tr(),
|
||||
'deleteSecret'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
@@ -231,6 +231,7 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
showConfirmAlert(
|
||||
'deleteCustomAppHint'.tr(),
|
||||
'deleteCustomApp'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.read(
|
||||
|
||||
@@ -159,9 +159,11 @@ class BotKeysScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
void revokeKey(String keyId) {
|
||||
showConfirmAlert('revokeBotKeyHint'.tr(), 'revokeBotKey'.tr()).then((
|
||||
confirm,
|
||||
) {
|
||||
showConfirmAlert(
|
||||
'revokeBotKeyHint'.tr(),
|
||||
'revokeBotKey'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.read(apiClientProvider);
|
||||
client
|
||||
|
||||
@@ -172,6 +172,7 @@ class BotsScreen extends HookConsumerWidget {
|
||||
showConfirmAlert(
|
||||
'deleteBotHint'.tr(),
|
||||
'deleteBot'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
@@ -631,6 +631,7 @@ class _ProjectListTile extends HookConsumerWidget {
|
||||
showConfirmAlert(
|
||||
'deleteProjectHint'.tr(),
|
||||
'deleteProject'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
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:flutter/foundation.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/extended_refresh_indicator.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:super_sliver_list/super_sliver_list.dart';
|
||||
|
||||
part 'explore.g.dart';
|
||||
|
||||
@@ -239,23 +242,74 @@ class ExploreScreen extends HookConsumerWidget {
|
||||
|
||||
final appBar = isWide ? null : _buildAppBar(tabController, context);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: appBar,
|
||||
body:
|
||||
isWide
|
||||
? _buildWideBody(
|
||||
context,
|
||||
ref,
|
||||
filterBar,
|
||||
user,
|
||||
notificationCount,
|
||||
query,
|
||||
events,
|
||||
selectedDay,
|
||||
currentFilter.value,
|
||||
)
|
||||
: _buildNarrowBody(context, ref, currentFilter.value),
|
||||
final dragging = useState(false);
|
||||
|
||||
return DropTarget(
|
||||
onDragDone: (detail) {
|
||||
dragging.value = false;
|
||||
if (detail.files.isNotEmpty) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder: (context) => ShareSheet.files(files: detail.files),
|
||||
);
|
||||
}
|
||||
},
|
||||
onDragEntered: (_) => dragging.value = true,
|
||||
onDragExited: (_) => dragging.value = false,
|
||||
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 contentWidget = switch (type) {
|
||||
'post' => ListView.separated(
|
||||
'post' => SuperListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (context, index) => const Gap(12),
|
||||
|
||||
@@ -256,21 +256,25 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ComposeFormFields(
|
||||
state: state,
|
||||
showPublisherAvatar: false,
|
||||
onPublisherTap: () {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const PublisherModal(),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
state.currentPublisher.value = value;
|
||||
}
|
||||
});
|
||||
},
|
||||
).padding(top: 16),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: ComposeFormFields(
|
||||
state: state,
|
||||
showPublisherAvatar: false,
|
||||
onPublisherTap: () {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const PublisherModal(),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
state.currentPublisher.value = value;
|
||||
}
|
||||
});
|
||||
},
|
||||
).padding(top: 16),
|
||||
),
|
||||
),
|
||||
|
||||
// Attachments preview
|
||||
ValueListenableBuilder<List<UniversalFile>>(
|
||||
|
||||
@@ -145,9 +145,11 @@ class PostActionButtons extends HookConsumerWidget {
|
||||
message: 'delete'.tr(),
|
||||
child: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
showConfirmAlert('deletePostHint'.tr(), 'deletePost'.tr()).then((
|
||||
confirm,
|
||||
) {
|
||||
showConfirmAlert(
|
||||
'deletePostHint'.tr(),
|
||||
'deletePost'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
client
|
||||
|
||||
@@ -427,6 +427,7 @@ class _RealmActionMenu extends HookConsumerWidget {
|
||||
showConfirmAlert(
|
||||
'deleteRealmHint'.tr(),
|
||||
'deleteRealm'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
|
||||
@@ -150,6 +150,7 @@ class AccountSessionSheet extends HookConsumerWidget {
|
||||
final confirm = await showConfirmAlert(
|
||||
'authDeviceLogoutHint'.tr(),
|
||||
'authDeviceLogout'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
if (!confirm || !context.mounted) return;
|
||||
try {
|
||||
@@ -276,6 +277,7 @@ class AccountSessionSheet extends HookConsumerWidget {
|
||||
final confirm = await showConfirmAlert(
|
||||
'authDeviceLogoutHint'.tr(),
|
||||
'authDeviceLogout'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
if (confirm && context.mounted) {
|
||||
try {
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/main.dart';
|
||||
import 'package:island/talker.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
||||
|
||||
@@ -156,6 +157,9 @@ String _parseRemoteError(DioException err) {
|
||||
return message ?? err.toString();
|
||||
}
|
||||
|
||||
// Track active overlay dialogs for dismissal
|
||||
final List<void Function()> _activeOverlayDialogs = [];
|
||||
|
||||
Future<T?> showOverlayDialog<T>({
|
||||
required Widget Function(BuildContext context, void Function(T? result) close)
|
||||
builder,
|
||||
@@ -174,6 +178,7 @@ Future<T?> showOverlayDialog<T>({
|
||||
}
|
||||
|
||||
entry.remove();
|
||||
_activeOverlayDialogs.remove(close);
|
||||
completer.complete(result);
|
||||
}
|
||||
|
||||
@@ -214,11 +219,24 @@ Future<T?> showOverlayDialog<T>({
|
||||
),
|
||||
);
|
||||
|
||||
_activeOverlayDialogs.add(() => close(null));
|
||||
globalOverlay.currentState!.insert(entry);
|
||||
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) {
|
||||
talker.error('Something went wrong...', err, err.stackTrace);
|
||||
}
|
||||
@@ -231,51 +249,128 @@ void showErrorAlert(dynamic err) {
|
||||
|
||||
showOverlayDialog<void>(
|
||||
builder:
|
||||
(context, close) => AlertDialog(
|
||||
title: Text('somethingWentWrong'.tr()),
|
||||
content: Text(text),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => close(null),
|
||||
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||
(context, close) => ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
|
||||
child: AlertDialog(
|
||||
title: null,
|
||||
titlePadding: EdgeInsets.zero,
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
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>(
|
||||
builder:
|
||||
(context, close) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => close(null),
|
||||
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||
(context, close) => ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
|
||||
child: AlertDialog(
|
||||
title: null,
|
||||
titlePadding: EdgeInsets.zero,
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
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>(
|
||||
builder:
|
||||
(context, close) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => close(false),
|
||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||
(context, close) => ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
|
||||
child: AlertDialog(
|
||||
title: null,
|
||||
titlePadding: EdgeInsets.zero,
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
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(
|
||||
onPressed: () => close(true),
|
||||
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||
),
|
||||
],
|
||||
actions: [
|
||||
TextButton(
|
||||
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;
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:island/route.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/upload_overlay.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -364,6 +365,12 @@ class PopAction extends Action<PopIntent> {
|
||||
|
||||
@override
|
||||
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()) {
|
||||
ref.read(routerProvider).pop();
|
||||
}
|
||||
|
||||
@@ -596,6 +596,7 @@ class MessageHoverActionMenu extends StatelessWidget {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'deleteMessageConfirmation'.tr(),
|
||||
'deleteMessage'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
|
||||
@@ -15,6 +15,7 @@ class NetworkStatusSheet extends HookConsumerWidget {
|
||||
final wsState = ref.watch(websocketStateProvider);
|
||||
|
||||
return SheetScaffold(
|
||||
heightFactor: 0.4,
|
||||
titleText:
|
||||
wsState == WebSocketState.connected()
|
||||
? 'Connection Status'
|
||||
|
||||
@@ -512,6 +512,7 @@ class FileListView extends HookConsumerWidget {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'Are you sure you want to delete the selected files?',
|
||||
'Delete Selected Files',
|
||||
isDanger: true,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
if (context.mounted) {
|
||||
@@ -742,22 +743,25 @@ class FileListView extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: onPickAndUpload,
|
||||
icon: const Icon(Symbols.upload_file),
|
||||
label: const Text('Upload Files'),
|
||||
),
|
||||
const Gap(12),
|
||||
OutlinedButton.icon(
|
||||
onPressed:
|
||||
() => onShowCreateDirectory(ref.context, currentPath),
|
||||
icon: const Icon(Symbols.create_new_folder),
|
||||
label: const Text('Create Directory'),
|
||||
),
|
||||
],
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: onPickAndUpload,
|
||||
icon: const Icon(Symbols.upload_file),
|
||||
label: const Text('Upload Files'),
|
||||
),
|
||||
const Gap(12),
|
||||
OutlinedButton.icon(
|
||||
onPressed:
|
||||
() => 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(
|
||||
'confirmDeleteFile'.tr(),
|
||||
'deleteFile'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
@@ -1153,6 +1158,7 @@ class FileListView extends HookConsumerWidget {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteFile'.tr(),
|
||||
'deleteFile'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
@@ -1221,6 +1227,7 @@ class FileListView extends HookConsumerWidget {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteFile'.tr(),
|
||||
'deleteFile'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
@@ -1263,6 +1270,7 @@ class FileListView extends HookConsumerWidget {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteFile'.tr(),
|
||||
'deleteFile'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/post.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/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/post/compose_card.dart';
|
||||
@@ -50,19 +51,33 @@ class PostComposeSheet extends HookConsumerWidget {
|
||||
final restoredInitialState = useState<PostComposeInitialState?>(null);
|
||||
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 =
|
||||
initialState?.forwardingTo ?? originalPost?.forwardedPost;
|
||||
initialState?.forwardingTo ?? effectiveOriginalPost?.forwardedPost;
|
||||
|
||||
// Create compose state
|
||||
final ComposeState state = useMemoized(
|
||||
() => ComposeLogic.createState(
|
||||
originalPost: originalPost,
|
||||
originalPost: effectiveOriginalPost,
|
||||
forwardedPost: forwardedPost,
|
||||
repliedPost: repliedPost,
|
||||
postType: 0,
|
||||
),
|
||||
[originalPost, forwardedPost, repliedPost],
|
||||
[effectiveOriginalPost, forwardedPost, repliedPost],
|
||||
);
|
||||
|
||||
// Add a listener to the entire state to trigger rebuilds
|
||||
@@ -112,7 +127,7 @@ class PostComposeSheet extends HookConsumerWidget {
|
||||
ref,
|
||||
state,
|
||||
context,
|
||||
originalPost: originalPost,
|
||||
originalPost: effectiveOriginalPost,
|
||||
repliedPost: repliedPost,
|
||||
forwardedPost: forwardedPost,
|
||||
onSuccess: () {
|
||||
@@ -139,8 +154,13 @@ class PostComposeSheet extends HookConsumerWidget {
|
||||
height: 24,
|
||||
child: const CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Icon(originalPost != null ? Symbols.edit : Symbols.upload),
|
||||
tooltip: originalPost != null ? 'postUpdate'.tr() : 'postPublish'.tr(),
|
||||
: Icon(
|
||||
effectiveOriginalPost != null ? Symbols.edit : Symbols.upload,
|
||||
),
|
||||
tooltip:
|
||||
effectiveOriginalPost != null
|
||||
? 'postUpdate'.tr()
|
||||
: 'postPublish'.tr(),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -148,7 +168,7 @@ class PostComposeSheet extends HookConsumerWidget {
|
||||
titleText: 'postCompose'.tr(),
|
||||
actions: actions,
|
||||
child: PostComposeCard(
|
||||
originalPost: originalPost,
|
||||
originalPost: effectiveOriginalPost,
|
||||
initialState: restoredInitialState.value ?? initialState,
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
onSubmit: () {
|
||||
|
||||
@@ -122,6 +122,7 @@ class DraftManagerSheet extends HookConsumerWidget {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'clearAllDraftsConfirm'.tr(),
|
||||
'clearAllDrafts'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
|
||||
@@ -197,6 +197,7 @@ class PostActionableItem extends HookConsumerWidget {
|
||||
showConfirmAlert(
|
||||
'deletePostHint'.tr(),
|
||||
'deletePost'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
|
||||
@@ -69,22 +69,24 @@ class PostItemCreator extends HookConsumerWidget {
|
||||
title: 'delete'.tr(),
|
||||
image: MenuImage.icon(Symbols.delete),
|
||||
callback: () {
|
||||
showConfirmAlert('deletePostHint'.tr(), 'deletePost'.tr()).then(
|
||||
(confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
client
|
||||
.delete('/sphere/posts/${item.id}')
|
||||
.catchError((err) {
|
||||
showErrorAlert(err);
|
||||
return err;
|
||||
})
|
||||
.then((_) {
|
||||
onRefresh?.call();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
showConfirmAlert(
|
||||
'deletePostHint'.tr(),
|
||||
'deletePost'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
client
|
||||
.delete('/sphere/posts/${item.id}')
|
||||
.catchError((err) {
|
||||
showErrorAlert(err);
|
||||
return err;
|
||||
})
|
||||
.then((_) {
|
||||
onRefresh?.call();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
MenuSeparator(),
|
||||
|
||||
@@ -196,7 +196,7 @@ class PostReplyPreview extends HookConsumerWidget {
|
||||
: (featuredReply!).map(
|
||||
data:
|
||||
(data) => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
ProfilePictureWidget(
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
@@ -177,8 +178,11 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
|
||||
|
||||
// Show compose sheet
|
||||
if (mounted) {
|
||||
PostComposeSheet.show(context, initialState: initialState);
|
||||
Navigator.of(context).pop(); // Close the share sheet
|
||||
await PostComposeSheet.show(context, initialState: initialState);
|
||||
// Close the share sheet after the compose sheet is dismissed
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
@@ -281,23 +285,10 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
|
||||
);
|
||||
|
||||
// Show navigation prompt
|
||||
final shouldNavigate = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
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()),
|
||||
),
|
||||
],
|
||||
),
|
||||
final shouldNavigate = await showConfirmAlert(
|
||||
'wouldYouLikeToGoToChat'.tr(),
|
||||
'shareSuccess'.tr(),
|
||||
icon: Symbols.check_circle,
|
||||
);
|
||||
|
||||
// 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 {
|
||||
try {
|
||||
String textToCopy = '';
|
||||
@@ -452,6 +529,15 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
|
||||
onTap: _isLoading ? null : _shareToPost,
|
||||
),
|
||||
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(
|
||||
icon: Symbols.content_copy,
|
||||
title: 'copy'.tr(),
|
||||
@@ -650,19 +736,26 @@ class _ChatRoomsList extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatRoomOption extends StatelessWidget {
|
||||
class _ChatRoomOption extends HookConsumerWidget {
|
||||
final SnChatRoom room;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _ChatRoomOption({required this.room, this.onTap});
|
||||
|
||||
@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 displayName =
|
||||
room.name ??
|
||||
(isDirect && room.members != null
|
||||
? room.members!.map((m) => m.account.nick).join(', ')
|
||||
(isDirect
|
||||
? validMembers.map((m) => m.account.nick).join(', ')
|
||||
: 'unknownChat'.tr());
|
||||
|
||||
return GestureDetector(
|
||||
@@ -694,18 +787,22 @@ class _ChatRoomOption extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child:
|
||||
room.picture != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: CloudFileWidget(
|
||||
item: room.picture!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
(isDirect && room.picture?.id == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId:
|
||||
validMembers
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
.toList(),
|
||||
radius: 16,
|
||||
)
|
||||
: Icon(
|
||||
isDirect ? Symbols.person : Symbols.group,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
: room.picture?.id == null
|
||||
? CircleAvatar(
|
||||
radius: 16,
|
||||
child: Text(room.name![0].toUpperCase()),
|
||||
)
|
||||
: ProfilePictureWidget(
|
||||
fileId: room.picture?.id,
|
||||
radius: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
@@ -72,26 +72,10 @@ class FileManagementActionSection extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
Future<void> _purgeFiles(BuildContext context, WidgetRef ref) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
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()),
|
||||
),
|
||||
],
|
||||
),
|
||||
final confirmed = await showConfirmAlert(
|
||||
'purgeFilesConfirm'.tr(),
|
||||
'confirmPurge'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
@@ -82,7 +82,7 @@ class SiteActionMenu extends HookConsumerWidget {
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/zone/sites/${site.id}');
|
||||
await client.delete('/zone/sites/$pubName/${site.slug}');
|
||||
if (context.mounted) {
|
||||
showSnackBar('siteDeletedSuccess'.tr());
|
||||
Navigator.of(context).pop();
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/publication_site.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/info_row.dart';
|
||||
import 'package:island/widgets/sites/site_info_card.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:material_symbols_icons/symbols.dart';
|
||||
import 'package:island/screens/creators/sites/site_detail.dart';
|
||||
|
||||
class SiteDetailContent extends HookConsumerWidget {
|
||||
@@ -24,8 +21,6 @@ class SiteDetailContent extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return ExtendedRefreshIndicator(
|
||||
onRefresh:
|
||||
() async =>
|
||||
@@ -36,65 +31,7 @@ class SiteDetailContent extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Site Info Card
|
||||
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: '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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SiteInfoCard(site: site),
|
||||
const Gap(8),
|
||||
if (site.mode == 1) // Self-Managed only
|
||||
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
|
||||
# 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.
|
||||
version: 3.3.0+148
|
||||
version: 3.4.0+149
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.2
|
||||
|
||||
Reference in New Issue
Block a user