9dfa06ffee
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>
346 lines
12 KiB
Dart
346 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
|
|
|
import '../../shared/theme/theme.dart';
|
|
import '../../shared/l10n/l10n.dart';
|
|
import '../../shared/widgets/buzz_loading_indicator.dart';
|
|
import 'channel.dart';
|
|
import 'channel_management_provider.dart';
|
|
import 'channel_mutes/channel_mutes_provider.dart';
|
|
|
|
class ManageChannelSheet extends HookConsumerWidget {
|
|
final Channel channel;
|
|
|
|
const ManageChannelSheet({super.key, required this.channel});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final canvasAsync = ref.watch(channelCanvasProvider(channel.id));
|
|
final isEditingCanvas = useState(false);
|
|
final isSavingCanvas = useState(false);
|
|
final isBusy = useState(false);
|
|
final actionError = useState<String?>(null);
|
|
final canvasController = useTextEditingController();
|
|
|
|
useEffect(() {
|
|
final canvas = canvasAsync.asData?.value;
|
|
if (!isEditingCanvas.value) {
|
|
canvasController.text = canvas?.content ?? '';
|
|
}
|
|
return null;
|
|
}, [canvasAsync.asData?.value.content, isEditingCanvas.value]);
|
|
|
|
final mutesState = ref.watch(channelMutesProvider);
|
|
final isMuted = mutesState.store.channels[channel.id]?.muted == true;
|
|
|
|
final canJoin =
|
|
channel.visibility == 'open' &&
|
|
!channel.isArchived &&
|
|
!channel.isMember &&
|
|
!channel.isDm;
|
|
final canLeave = channel.isMember && !channel.isArchived && !channel.isDm;
|
|
final canEditCanvas = channel.isMember && !channel.isArchived;
|
|
|
|
Future<void> joinChannel() async {
|
|
if (isBusy.value) return;
|
|
isBusy.value = true;
|
|
actionError.value = null;
|
|
try {
|
|
await ref.read(channelActionsProvider).joinChannel(channel.id);
|
|
if (context.mounted) {
|
|
Navigator.of(context).pop(false);
|
|
}
|
|
} catch (error) {
|
|
actionError.value = error.toString();
|
|
} finally {
|
|
isBusy.value = false;
|
|
}
|
|
}
|
|
|
|
Future<void> leaveChannel() async {
|
|
if (isBusy.value) return;
|
|
isBusy.value = true;
|
|
actionError.value = null;
|
|
try {
|
|
await ref.read(channelActionsProvider).leaveChannel(channel.id);
|
|
if (context.mounted) {
|
|
Navigator.of(context).pop(true);
|
|
}
|
|
} catch (error) {
|
|
actionError.value = error.toString();
|
|
} finally {
|
|
isBusy.value = false;
|
|
}
|
|
}
|
|
|
|
Future<void> saveCanvas() async {
|
|
if (isSavingCanvas.value) {
|
|
return;
|
|
}
|
|
isSavingCanvas.value = true;
|
|
actionError.value = null;
|
|
try {
|
|
await ref
|
|
.read(channelActionsProvider)
|
|
.setCanvas(
|
|
channelId: channel.id,
|
|
content: canvasController.text.trim(),
|
|
);
|
|
if (context.mounted) {
|
|
isEditingCanvas.value = false;
|
|
}
|
|
} catch (error) {
|
|
actionError.value = error.toString();
|
|
} finally {
|
|
isSavingCanvas.value = false;
|
|
}
|
|
}
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.fromLTRB(
|
|
Grid.gutter,
|
|
0,
|
|
Grid.gutter,
|
|
MediaQuery.viewInsetsOf(context).bottom + Grid.xs,
|
|
),
|
|
child: SafeArea(
|
|
top: false,
|
|
child: ListView(
|
|
shrinkWrap: true,
|
|
children: [
|
|
Text(
|
|
context.l10n.manageChannel,
|
|
style: context.textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: Grid.xxs),
|
|
Text(
|
|
context.l10n.basicManagement(channel.name),
|
|
style: context.textTheme.bodySmall?.copyWith(
|
|
color: context.colors.onSurfaceVariant,
|
|
),
|
|
),
|
|
if (actionError.value case final error?) ...[
|
|
const SizedBox(height: Grid.xs),
|
|
Text(
|
|
error,
|
|
style: context.textTheme.bodySmall?.copyWith(
|
|
color: context.colors.error,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: Grid.xs),
|
|
SwitchListTile(
|
|
title: Text(context.l10n.muteChannel),
|
|
subtitle: Text(context.l10n.suppressNotifications),
|
|
secondary: const Icon(LucideIcons.bellOff),
|
|
contentPadding: EdgeInsets.zero,
|
|
value: isMuted,
|
|
onChanged: (value) {
|
|
if (value) {
|
|
ref
|
|
.read(channelMutesProvider.notifier)
|
|
.muteChannel(channel.id);
|
|
} else {
|
|
ref
|
|
.read(channelMutesProvider.notifier)
|
|
.unmuteChannel(channel.id);
|
|
}
|
|
},
|
|
),
|
|
if (canJoin || canLeave) ...[
|
|
const SizedBox(height: Grid.xs),
|
|
Wrap(
|
|
spacing: Grid.xxs,
|
|
children: [
|
|
if (canJoin)
|
|
FilledButton.tonal(
|
|
onPressed: isBusy.value ? null : joinChannel,
|
|
child: Text(
|
|
isBusy.value
|
|
? context.l10n.joining
|
|
: context.l10n.joinChannel,
|
|
),
|
|
),
|
|
if (canLeave)
|
|
OutlinedButton(
|
|
onPressed: isBusy.value ? null : leaveChannel,
|
|
child: Text(
|
|
isBusy.value
|
|
? context.l10n.leaving
|
|
: context.l10n.leaveChannelAction,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
const SizedBox(height: Grid.sm),
|
|
Text(context.l10n.context, style: context.textTheme.labelLarge),
|
|
const SizedBox(height: Grid.xxs),
|
|
_ContextCard(
|
|
label: context.l10n.description,
|
|
value: channel.description,
|
|
emptyLabel: context.l10n.noDescription,
|
|
),
|
|
const SizedBox(height: Grid.xxs),
|
|
_ContextCard(
|
|
label: context.l10n.topic,
|
|
value: channel.topic,
|
|
emptyLabel: context.l10n.noTopic,
|
|
),
|
|
const SizedBox(height: Grid.xxs),
|
|
_ContextCard(
|
|
label: context.l10n.purpose,
|
|
value: channel.purpose,
|
|
emptyLabel: context.l10n.noPurpose,
|
|
),
|
|
if (!channel.isDm) ...[
|
|
const SizedBox(height: Grid.sm),
|
|
Text(context.l10n.canvas, style: context.textTheme.labelLarge),
|
|
const SizedBox(height: Grid.xxs),
|
|
canvasAsync.when(
|
|
data: (canvas) {
|
|
if (isEditingCanvas.value) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
TextField(
|
|
controller: canvasController,
|
|
maxLines: 8,
|
|
minLines: 6,
|
|
decoration: InputDecoration(
|
|
hintText: context.l10n.canvasHint,
|
|
),
|
|
),
|
|
const SizedBox(height: Grid.xxs),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: isSavingCanvas.value
|
|
? null
|
|
: () {
|
|
isEditingCanvas.value = false;
|
|
canvasController.text =
|
|
canvas.content ?? '';
|
|
},
|
|
child: Text(context.l10n.cancel),
|
|
),
|
|
const SizedBox(width: Grid.half),
|
|
FilledButton(
|
|
onPressed: isSavingCanvas.value
|
|
? null
|
|
: saveCanvas,
|
|
child: Text(
|
|
isSavingCanvas.value
|
|
? context.l10n.saving
|
|
: context.l10n.saveCanvas,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(Grid.xs),
|
|
decoration: BoxDecoration(
|
|
color: context.colors.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
),
|
|
child: Text(
|
|
canvas.content?.trim().isNotEmpty == true
|
|
? canvas.content!
|
|
: context.l10n.noCanvas,
|
|
style: context.textTheme.bodyMedium?.copyWith(
|
|
color: context.colors.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: Grid.xxs),
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: FilledButton.tonal(
|
|
onPressed: canEditCanvas
|
|
? () => isEditingCanvas.value = true
|
|
: null,
|
|
child: Text(
|
|
canvas.content?.trim().isNotEmpty == true
|
|
? context.l10n.editCanvas
|
|
: context.l10n.createCanvas,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
loading: () => Center(
|
|
child: BuzzLoadingIndicator(
|
|
size: 40,
|
|
semanticLabel: context.l10n.loadingChannelDetails,
|
|
),
|
|
),
|
|
error: (error, _) => Text(
|
|
error.toString(),
|
|
style: context.textTheme.bodySmall?.copyWith(
|
|
color: context.colors.error,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ContextCard extends StatelessWidget {
|
|
final String label;
|
|
final String? value;
|
|
final String emptyLabel;
|
|
|
|
const _ContextCard({
|
|
required this.label,
|
|
required this.value,
|
|
required this.emptyLabel,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(Grid.xs),
|
|
decoration: BoxDecoration(
|
|
color: context.colors.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: context.textTheme.labelSmall?.copyWith(
|
|
color: context.colors.onSurfaceVariant,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: Grid.half),
|
|
Text(
|
|
value?.trim().isNotEmpty == true ? value!.trim() : emptyLabel,
|
|
style: context.textTheme.bodyMedium?.copyWith(
|
|
color: context.colors.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|