Compare commits

...

15 Commits

41 changed files with 604 additions and 362 deletions

View File

@@ -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"
} }

View File

@@ -585,10 +585,10 @@
"unknownChat": "未知聊天", "unknownChat": "未知聊天",
"addAdditionalMessage": "添加附加消息……", "addAdditionalMessage": "添加附加消息……",
"uploadingFiles": "上传文件中……", "uploadingFiles": "上传文件中……",
"sharedSuccessfully": "分享成功", "sharedSuccessfully": "分享成功",
"shareSuccess": "分享成功", "shareSuccess": "分享成功",
"shareToSpecificChatSuccess": "成功分享至 {}", "shareToSpecificChatSuccess": "成功分享至 {}",
"wouldYouLikeToGoToChat": "是否前往该聊天?", "wouldYouLikeToGoToChat": "是否前往该聊天页面",
"no": "否", "no": "否",
"yes": "是", "yes": "是",
"navigateToChat": "前往聊天", "navigateToChat": "前往聊天",

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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);

View File

@@ -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(

View File

@@ -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(

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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(

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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),

View File

@@ -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>>(

View File

@@ -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

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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();
} }

View File

@@ -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) {

View File

@@ -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'

View File

@@ -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;

View File

@@ -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: () {

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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(),

View File

@@ -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(

View File

@@ -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),

View File

@@ -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;

View File

@@ -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();

View File

@@ -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),

View 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,
),
],
),
),
);
}
}

View File

@@ -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