feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
@@ -0,0 +1,593 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
import '../../shared/clipboard_utils.dart';
|
||||
import '../../shared/mentions/agent_identity_provider.dart';
|
||||
import '../../shared/l10n/l10n.dart';
|
||||
import '../../shared/theme/theme.dart';
|
||||
import '../../shared/widgets/buzz_loading_indicator.dart';
|
||||
import '../../shared/widgets/modal_presentation.dart';
|
||||
import '../../shared/widgets/sheet_divider.dart';
|
||||
import 'channel.dart';
|
||||
import 'channel_management_provider.dart';
|
||||
import 'channel_mutes/channel_mutes_provider.dart';
|
||||
import 'channel_sections/channel_sections_provider.dart';
|
||||
import 'channel_stars/channel_stars_provider.dart';
|
||||
import 'channels_provider.dart';
|
||||
import 'manage_channel_sheet.dart';
|
||||
import '../../shared/read_state/read_state_provider.dart';
|
||||
import '../../shared/read_state/read_state_time.dart';
|
||||
|
||||
/// Opens the mobile channel actions sheet and returns whether its parent page
|
||||
/// should close after a successful lifecycle action.
|
||||
Future<bool?> showChannelActionsSheet({
|
||||
required BuildContext context,
|
||||
required Channel channel,
|
||||
required bool isUnread,
|
||||
VoidCallback? onMarkRead,
|
||||
String? sectionId,
|
||||
}) => showBuzzModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 640,
|
||||
maxHeight: MediaQuery.sizeOf(context).height * 0.7,
|
||||
),
|
||||
builder: (_) => ChannelActionsSheet(
|
||||
channel: channel,
|
||||
isUnread: isUnread,
|
||||
onMarkRead: onMarkRead,
|
||||
sectionId: sectionId,
|
||||
),
|
||||
);
|
||||
|
||||
/// Mobile action sheet for channel-level read, organization, and lifecycle
|
||||
/// operations.
|
||||
class ChannelActionsSheet extends ConsumerWidget {
|
||||
const ChannelActionsSheet({
|
||||
super.key,
|
||||
required this.channel,
|
||||
required this.isUnread,
|
||||
this.onMarkRead,
|
||||
this.sectionId,
|
||||
});
|
||||
|
||||
final Channel channel;
|
||||
final bool isUnread;
|
||||
final VoidCallback? onMarkRead;
|
||||
final String? sectionId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isMuted =
|
||||
ref.watch(channelMutesProvider).store.channels[channel.id]?.muted ==
|
||||
true;
|
||||
final isStarred =
|
||||
!channel.isDm &&
|
||||
ref.watch(channelStarsProvider).store.channels[channel.id]?.starred ==
|
||||
true;
|
||||
final membersAsync = channel.isDm
|
||||
? const AsyncValue<List<ChannelMember>>.data([])
|
||||
: ref.watch(channelMembersProvider(channel.id));
|
||||
final agentOwnersAsync = channel.isDm
|
||||
? const AsyncValue<Map<String, String>>.data({})
|
||||
: ref.watch(agentOwnersProvider);
|
||||
final currentPubkey = ref.watch(currentPubkeyProvider)?.toLowerCase();
|
||||
final currentMember = membersAsync.value?.cast<ChannelMember?>().firstWhere(
|
||||
(member) => member?.pubkey.toLowerCase() == currentPubkey,
|
||||
orElse: () => null,
|
||||
);
|
||||
final ownsOwnerAgent =
|
||||
currentPubkey != null &&
|
||||
membersAsync.value?.any(
|
||||
(member) =>
|
||||
member.isOwner &&
|
||||
agentOwnersAsync.value?[member.pubkey.toLowerCase()]
|
||||
?.toLowerCase() ==
|
||||
currentPubkey,
|
||||
) ==
|
||||
true;
|
||||
final canManageLifecycle =
|
||||
currentMember?.isElevated == true || ownsOwnerAgent;
|
||||
final canArchive = !channel.isArchived && canManageLifecycle;
|
||||
final canUnarchive = channel.isArchived && canManageLifecycle;
|
||||
final canDelete =
|
||||
!channel.isArchived &&
|
||||
(currentMember?.isOwner == true || ownsOwnerAgent);
|
||||
final lifecycleCapabilitiesLoading =
|
||||
membersAsync.isLoading || agentOwnersAsync.isLoading;
|
||||
final lifecycleCapabilitiesUnavailable =
|
||||
membersAsync.hasError || agentOwnersAsync.hasError;
|
||||
|
||||
void close() => Navigator.of(context).pop();
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: IconTheme.merge(
|
||||
data: const IconThemeData(size: 22),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
Grid.gutter,
|
||||
0,
|
||||
Grid.gutter,
|
||||
Grid.xs,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!channel.isDm) ...[
|
||||
_ChannelQuickActionsRow(
|
||||
isStarred: isStarred,
|
||||
isUnread: isUnread,
|
||||
onToggleStar: () {
|
||||
close();
|
||||
final notifier = ref.read(channelStarsProvider.notifier);
|
||||
isStarred
|
||||
? notifier.unstarChannel(channel.id)
|
||||
: notifier.starChannel(channel.id);
|
||||
},
|
||||
onToggleRead: () {
|
||||
close();
|
||||
final timestamp = dateTimeToUnixSeconds(
|
||||
channel.lastMessageAt,
|
||||
);
|
||||
if (isUnread) {
|
||||
onMarkRead?.call();
|
||||
if (timestamp != null) {
|
||||
ref
|
||||
.read(readStateProvider.notifier)
|
||||
.markContextRead(
|
||||
channel.id,
|
||||
timestamp,
|
||||
clearForcedMessages: true,
|
||||
);
|
||||
ref
|
||||
.read(channelsProvider.notifier)
|
||||
.clearObservedUnreadCoveredByRead(
|
||||
channel.id,
|
||||
timestamp,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ref
|
||||
.read(readStateProvider.notifier)
|
||||
.markContextUnread(channel.id, channelId: channel.id);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: Grid.xs),
|
||||
],
|
||||
if (!channel.isDm)
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(LucideIcons.folderInput),
|
||||
title: Text(context.l10n.moveToSection),
|
||||
onTap: () async {
|
||||
final pageContext = Navigator.of(
|
||||
context,
|
||||
rootNavigator: true,
|
||||
).context;
|
||||
close();
|
||||
await _showMoveSectionSheet(
|
||||
pageContext,
|
||||
ref,
|
||||
channel: channel,
|
||||
sectionId: sectionId,
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(isMuted ? LucideIcons.bell : LucideIcons.bellOff),
|
||||
title: Text(
|
||||
isMuted
|
||||
? context.l10n.unmuteChannel
|
||||
: context.l10n.muteChannel,
|
||||
),
|
||||
onTap: () {
|
||||
close();
|
||||
final notifier = ref.read(channelMutesProvider.notifier);
|
||||
isMuted
|
||||
? notifier.unmuteChannel(channel.id)
|
||||
: notifier.muteChannel(channel.id);
|
||||
},
|
||||
),
|
||||
if (!channel.isDm)
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(LucideIcons.settings),
|
||||
title: Text(context.l10n.manageChannel),
|
||||
onTap: () async {
|
||||
final shouldClose = await showBuzzModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 640,
|
||||
maxHeight: MediaQuery.sizeOf(context).height * 0.9,
|
||||
),
|
||||
builder: (_) => ManageChannelSheet(channel: channel),
|
||||
);
|
||||
if (shouldClose == true && context.mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(LucideIcons.copy),
|
||||
title: Text(context.l10n.copyChannelName),
|
||||
onTap: () {
|
||||
close();
|
||||
copyToClipboard(
|
||||
context,
|
||||
channel.name,
|
||||
message: context.l10n.channelNameCopied,
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(LucideIcons.hash),
|
||||
title: Text(context.l10n.copyChannelId),
|
||||
onTap: () {
|
||||
close();
|
||||
copyToClipboard(
|
||||
context,
|
||||
channel.id,
|
||||
message: context.l10n.channelIdCopied,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!channel.isDm) ...[
|
||||
const SheetDivider(),
|
||||
if (channel.isMember && !channel.isArchived)
|
||||
_ActionTile(
|
||||
icon: LucideIcons.logOut,
|
||||
label: context.l10n.leaveChannelLabel,
|
||||
destructive: true,
|
||||
onTap: () => _confirmAndRun(
|
||||
context,
|
||||
ref,
|
||||
title: context.l10n.leaveChannel(channel.name),
|
||||
body: context.l10n.leaveChannelBody,
|
||||
confirmLabel: context.l10n.leave,
|
||||
action: () => ref
|
||||
.read(channelActionsProvider)
|
||||
.leaveChannel(channel.id),
|
||||
),
|
||||
),
|
||||
if (lifecycleCapabilitiesLoading)
|
||||
ListTile(
|
||||
enabled: false,
|
||||
leading: BuzzLoadingIndicator(
|
||||
size: 20,
|
||||
semanticLabel: context.l10n.loadingChannelActions,
|
||||
),
|
||||
title: Text(context.l10n.loadingChannelActions),
|
||||
)
|
||||
else if (lifecycleCapabilitiesUnavailable)
|
||||
ListTile(
|
||||
enabled: false,
|
||||
leading: Icon(LucideIcons.triangleAlert),
|
||||
title: Text(context.l10n.channelActionsUnavailable),
|
||||
)
|
||||
else ...[
|
||||
if (canArchive)
|
||||
_ActionTile(
|
||||
icon: LucideIcons.archive,
|
||||
label: context.l10n.archiveChannelLabel,
|
||||
onTap: () => _confirmAndRun(
|
||||
context,
|
||||
ref,
|
||||
title: context.l10n.archiveChannel(channel.name),
|
||||
body: context.l10n.archiveChannelBody,
|
||||
confirmLabel: context.l10n.archive,
|
||||
action: () => ref
|
||||
.read(channelActionsProvider)
|
||||
.archiveChannel(channel.id),
|
||||
),
|
||||
),
|
||||
if (canUnarchive)
|
||||
_ActionTile(
|
||||
icon: LucideIcons.archiveRestore,
|
||||
label: context.l10n.unarchiveChannelLabel,
|
||||
onTap: () => _confirmAndRun(
|
||||
context,
|
||||
ref,
|
||||
title: context.l10n.unarchiveChannel(channel.name),
|
||||
body: context.l10n.unarchiveChannelBody,
|
||||
confirmLabel: context.l10n.unarchive,
|
||||
action: () => ref
|
||||
.read(channelActionsProvider)
|
||||
.unarchiveChannel(channel.id),
|
||||
),
|
||||
),
|
||||
if (canDelete)
|
||||
_ActionTile(
|
||||
icon: LucideIcons.trash2,
|
||||
label: context.l10n.deleteChannelLabel,
|
||||
destructive: true,
|
||||
onTap: () => _confirmAndRun(
|
||||
context,
|
||||
ref,
|
||||
title: context.l10n.deleteChannel(channel.name),
|
||||
body: context.l10n.deleteChannelBody,
|
||||
confirmLabel: context.l10n.delete,
|
||||
action: () => ref
|
||||
.read(channelActionsProvider)
|
||||
.deleteChannel(channel.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChannelQuickActionsRow extends StatelessWidget {
|
||||
const _ChannelQuickActionsRow({
|
||||
required this.isStarred,
|
||||
required this.isUnread,
|
||||
required this.onToggleStar,
|
||||
required this.onToggleRead,
|
||||
});
|
||||
|
||||
final bool isStarred;
|
||||
final bool isUnread;
|
||||
final VoidCallback onToggleStar;
|
||||
final VoidCallback onToggleRead;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _ChannelQuickAction(
|
||||
icon: isStarred ? LucideIcons.starOff : LucideIcons.star,
|
||||
label: isStarred ? context.l10n.unstar : context.l10n.star,
|
||||
onTap: onToggleStar,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Grid.twelve),
|
||||
Expanded(
|
||||
child: _ChannelQuickAction(
|
||||
icon: isUnread ? LucideIcons.checkCheck : LucideIcons.circleDot,
|
||||
label: isUnread ? context.l10n.markRead : context.l10n.markUnread,
|
||||
onTap: onToggleRead,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _ChannelQuickAction extends StatelessWidget {
|
||||
const _ChannelQuickAction({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
onTap: () {
|
||||
unawaited(HapticFeedback.lightImpact());
|
||||
onTap();
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
height: 68 + (Grid.xxs * 2),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(Radii.dialog),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 22, color: context.colors.onSurface),
|
||||
const SizedBox(height: Grid.xxs),
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ActionTile extends StatelessWidget {
|
||||
const _ActionTile({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
this.destructive = false,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
final bool destructive;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(icon, color: destructive ? context.colors.error : null),
|
||||
title: Text(
|
||||
label,
|
||||
style: destructive ? TextStyle(color: context.colors.error) : null,
|
||||
),
|
||||
onTap: () {
|
||||
unawaited(HapticFeedback.lightImpact());
|
||||
onTap();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmAndRun(
|
||||
BuildContext sheetContext,
|
||||
WidgetRef ref, {
|
||||
required String title,
|
||||
required String body,
|
||||
required String confirmLabel,
|
||||
required Future<void> Function() action,
|
||||
}) async {
|
||||
final pageContext = Navigator.of(sheetContext, rootNavigator: true).context;
|
||||
final confirmed = await showBuzzDialog<bool>(
|
||||
context: pageContext,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(body),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: Text(dialogContext.l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: Text(confirmLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
try {
|
||||
await action();
|
||||
if (pageContext.mounted) Navigator.of(pageContext).pop(true);
|
||||
} catch (error) {
|
||||
if (!pageContext.mounted) return;
|
||||
ScaffoldMessenger.of(pageContext).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
pageContext.l10n.actionFailed(confirmLabel),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showMoveSectionSheet(
|
||||
BuildContext context,
|
||||
WidgetRef ref, {
|
||||
required Channel channel,
|
||||
required String? sectionId,
|
||||
}) async {
|
||||
final sections = [...ref.read(channelSectionsProvider).store.sections]
|
||||
..sort((a, b) => a.order.compareTo(b.order));
|
||||
await showBuzzModalBottomSheet<void>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: IconTheme.merge(
|
||||
data: const IconThemeData(size: 22),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
Grid.gutter,
|
||||
0,
|
||||
Grid.gutter,
|
||||
Grid.xs,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final section in sections)
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.folder),
|
||||
title: Text(section.name),
|
||||
trailing: sectionId == section.id
|
||||
? Icon(
|
||||
LucideIcons.check,
|
||||
color: sheetContext.colors.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
ref
|
||||
.read(channelSectionsProvider.notifier)
|
||||
.assignChannel(channel.id, section.id);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.folderPlus),
|
||||
title: Text(context.l10n.newSection),
|
||||
onTap: () async {
|
||||
Navigator.of(sheetContext).pop();
|
||||
final name = await _showSectionNameDialog(context);
|
||||
if (name == null || name.isEmpty) return;
|
||||
final notifier = ref.read(channelSectionsProvider.notifier);
|
||||
notifier.createSection(name);
|
||||
final created = ref
|
||||
.read(channelSectionsProvider)
|
||||
.store
|
||||
.sections
|
||||
.where((section) => section.name == name.trim())
|
||||
.lastOrNull;
|
||||
if (created != null) {
|
||||
notifier.assignChannel(channel.id, created.id);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (sectionId != null)
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.folderMinus),
|
||||
title: Text(context.l10n.removeFromSection),
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
ref
|
||||
.read(channelSectionsProvider.notifier)
|
||||
.unassignChannel(channel.id);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> _showSectionNameDialog(BuildContext context) async {
|
||||
final controller = TextEditingController();
|
||||
final result = await showBuzzDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(context.l10n.newSectionTitle),
|
||||
content: TextField(controller: controller, autofocus: true),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(dialogContext).pop(controller.text.trim()),
|
||||
child: Text(context.l10n.create),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
controller.dispose();
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user