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>
553 lines
18 KiB
Dart
553 lines
18 KiB
Dart
part of '../channels_page.dart';
|
|
|
|
class _CommunitySwitcherSheet extends HookConsumerWidget {
|
|
const _CommunitySwitcherSheet();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final communitiesAsync = ref.watch(communityListProvider);
|
|
final activeAsync = ref.watch(activeCommunityProvider);
|
|
final isEditing = useState(false);
|
|
|
|
return SafeArea(
|
|
child: Column(
|
|
key: const Key('community-switcher-sheet'),
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(
|
|
Grid.gutter,
|
|
Grid.xxs,
|
|
Grid.gutter,
|
|
Grid.xxs,
|
|
),
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(minHeight: 32),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
context.l10n.switchCommunity,
|
|
key: const Key('community-switcher-title'),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: context.textTheme.titleLarge?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: Grid.half),
|
|
child: ConstrainedBox(
|
|
key: const Key('community-switcher-edit'),
|
|
constraints: const BoxConstraints(
|
|
minWidth: 48,
|
|
minHeight: 32,
|
|
),
|
|
child: TextButton(
|
|
onPressed: () => isEditing.value = !isEditing.value,
|
|
style: TextButton.styleFrom(
|
|
minimumSize: Size.zero,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: Grid.half,
|
|
),
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
),
|
|
child: Text(isEditing.value ? context.l10n.done : context.l10n.edit),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Flexible(
|
|
child: communitiesAsync.when(
|
|
loading: () => SizedBox(
|
|
height: 120,
|
|
child: Center(
|
|
child: BuzzLoadingIndicator(
|
|
size: 40,
|
|
semanticLabel: context.l10n.loadingCommunities,
|
|
),
|
|
),
|
|
),
|
|
error: (e, _) => Padding(
|
|
padding: const EdgeInsets.all(Grid.xs),
|
|
child: Text(context.l10n.communityLoadFailed(e.toString())),
|
|
),
|
|
data: (communities) {
|
|
final activeId = activeAsync.value?.id;
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.only(bottom: Grid.xs),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: Grid.gutter,
|
|
),
|
|
child: Material(
|
|
key: const Key('community-switcher-options'),
|
|
color: context.colors.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(Radii.card),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: Column(
|
|
children: [
|
|
for (
|
|
var index = 0;
|
|
index < communities.length;
|
|
index++
|
|
) ...[
|
|
if (index > 0) const _CommunitySwitcherDivider(),
|
|
_CommunitySwitcherTile(
|
|
community: communities[index],
|
|
isActive: communities[index].id == activeId,
|
|
isEditing: isEditing.value,
|
|
onTap: isEditing.value
|
|
? null
|
|
: () async {
|
|
final community = communities[index];
|
|
if (community.id != activeId) {
|
|
await ref
|
|
.read(
|
|
communityListProvider.notifier,
|
|
)
|
|
.switchCommunity(community.id);
|
|
}
|
|
if (context.mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
},
|
|
onRemove: () => _confirmRemoveCommunity(
|
|
context,
|
|
ref,
|
|
communities[index],
|
|
closeSheetAfterRemoval:
|
|
communities[index].id == activeId,
|
|
),
|
|
),
|
|
],
|
|
if (communities.isNotEmpty)
|
|
const _CommunitySwitcherDivider(),
|
|
_AddCommunityTile(
|
|
onTap: () {
|
|
final nav = Navigator.of(
|
|
context,
|
|
rootNavigator: true,
|
|
);
|
|
ref.read(pairingProvider.notifier).reset();
|
|
Navigator.of(context).pop();
|
|
nav.push(
|
|
MaterialPageRoute<void>(
|
|
builder: (_) =>
|
|
const PairingPage(addingCommunity: true),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
const double _communitySwitcherAvatarSize = 36;
|
|
|
|
class _CommunitySwitcherDivider extends StatelessWidget {
|
|
const _CommunitySwitcherDivider();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Divider(
|
|
height: 1,
|
|
thickness: 1,
|
|
indent: Grid.xs + _communitySwitcherAvatarSize + Grid.xs,
|
|
endIndent: 0,
|
|
color: context.colors.onSurface.withValues(alpha: 0.12),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CommunitySwitcherTile extends StatelessWidget {
|
|
final Community community;
|
|
final bool isActive;
|
|
final bool isEditing;
|
|
final VoidCallback? onTap;
|
|
final VoidCallback onRemove;
|
|
|
|
const _CommunitySwitcherTile({
|
|
required this.community,
|
|
required this.isActive,
|
|
required this.isEditing,
|
|
required this.onTap,
|
|
required this.onRemove,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final host = Uri.tryParse(community.relayUrl)?.host ?? community.relayUrl;
|
|
|
|
return InkWell(
|
|
key: Key('community-switcher-row-${community.id}'),
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(
|
|
Grid.xs,
|
|
Grid.twelve,
|
|
Grid.xxs,
|
|
Grid.twelve,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
_CommunityAvatar(
|
|
key: Key('community-switcher-avatar-${community.id}'),
|
|
name: community.name,
|
|
relayUrl: community.relayUrl,
|
|
size: _communitySwitcherAvatarSize,
|
|
),
|
|
const SizedBox(width: Grid.xs),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
community.name,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: context.textTheme.bodyLarge?.copyWith(
|
|
fontWeight: isActive
|
|
? FontWeight.w600
|
|
: FontWeight.normal,
|
|
),
|
|
),
|
|
const SizedBox(height: Grid.quarter),
|
|
Text(
|
|
host,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: context.textTheme.bodySmall?.copyWith(
|
|
color: context.colors.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: Grid.xxs),
|
|
_CommunityActionSwap(
|
|
key: Key('community-switcher-action-${community.id}'),
|
|
communityId: community.id,
|
|
communityName: community.name,
|
|
isActive: isActive,
|
|
isEditing: isEditing,
|
|
onRemove: onRemove,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
const _communityActionSwapDuration = Duration(milliseconds: 250);
|
|
const double _communityActionSwapBlur = 2;
|
|
const double _communityActionSwapStartScale = 0.25;
|
|
|
|
class _CommunityActionSwap extends StatelessWidget {
|
|
const _CommunityActionSwap({
|
|
super.key,
|
|
required this.communityId,
|
|
required this.communityName,
|
|
required this.isActive,
|
|
required this.isEditing,
|
|
required this.onRemove,
|
|
});
|
|
|
|
final String communityId;
|
|
final String communityName;
|
|
final bool isActive;
|
|
final bool isEditing;
|
|
final VoidCallback onRemove;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final reduceMotion =
|
|
MediaQuery.maybeOf(context)?.disableAnimations ?? false;
|
|
|
|
return SizedBox.square(
|
|
dimension: 40,
|
|
child: AnimatedSwitcher(
|
|
duration: reduceMotion ? Duration.zero : _communityActionSwapDuration,
|
|
reverseDuration: reduceMotion
|
|
? Duration.zero
|
|
: _communityActionSwapDuration,
|
|
transitionBuilder: (child, animation) {
|
|
final eased = CurvedAnimation(
|
|
parent: animation,
|
|
curve: Curves.easeInOut,
|
|
);
|
|
return AnimatedBuilder(
|
|
animation: eased,
|
|
child: child,
|
|
builder: (context, child) {
|
|
final progress = eased.value;
|
|
final blur = _communityActionSwapBlur * (1 - progress);
|
|
final scale =
|
|
_communityActionSwapStartScale +
|
|
((1 - _communityActionSwapStartScale) * progress);
|
|
return Opacity(
|
|
opacity: progress,
|
|
child: ImageFiltered(
|
|
enabled: blur > 0.01,
|
|
imageFilter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
|
|
child: Transform.scale(scale: scale, child: child),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
child: isEditing
|
|
? IconButton(
|
|
key: ValueKey('community-switcher-remove-$communityId'),
|
|
tooltip: context.l10n.removeNamedCommunity(communityName),
|
|
visualDensity: VisualDensity.compact,
|
|
onPressed: onRemove,
|
|
icon: Icon(
|
|
LucideIcons.trash2,
|
|
size: 18,
|
|
color: context.colors.error,
|
|
),
|
|
)
|
|
: Center(
|
|
key: ValueKey('community-switcher-selection-$communityId'),
|
|
child: _CommunitySelectionIndicator(
|
|
key: ValueKey('community-switcher-circle-$communityId'),
|
|
communityName: communityName,
|
|
isActive: isActive,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CommunitySelectionIndicator extends StatelessWidget {
|
|
const _CommunitySelectionIndicator({
|
|
super.key,
|
|
required this.communityName,
|
|
required this.isActive,
|
|
});
|
|
|
|
final String communityName;
|
|
final bool isActive;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Semantics(
|
|
label: isActive
|
|
? context.l10n.currentCommunity(communityName)
|
|
: context.l10n.switchToCommunity(communityName),
|
|
child: Container(
|
|
width: 24,
|
|
height: 24,
|
|
decoration: BoxDecoration(
|
|
color: isActive ? context.appColors.success : Colors.transparent,
|
|
border: isActive
|
|
? null
|
|
: Border.all(color: context.colors.outlineVariant, width: 2),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: isActive
|
|
? const Icon(LucideIcons.check, color: Colors.white, size: 16)
|
|
: null,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AddCommunityTile extends StatelessWidget {
|
|
const _AddCommunityTile({required this.onTap});
|
|
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
key: const Key('community-switcher-add'),
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: Grid.xs,
|
|
vertical: Grid.twelve,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: _communitySwitcherAvatarSize,
|
|
height: _communitySwitcherAvatarSize,
|
|
decoration: BoxDecoration(
|
|
color: context.colors.primaryContainer,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
LucideIcons.plus,
|
|
size: 18,
|
|
color: context.colors.onPrimaryContainer,
|
|
),
|
|
),
|
|
const SizedBox(width: Grid.xs),
|
|
Text(
|
|
context.l10n.addCommunity,
|
|
style: context.textTheme.bodyLarge?.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _confirmRemoveCommunity(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
Community community, {
|
|
required bool closeSheetAfterRemoval,
|
|
}) async {
|
|
final confirmed = await showBuzzDialog<bool>(
|
|
context: context,
|
|
builder: (dialogContext) => AlertDialog.adaptive(
|
|
title: Text(context.l10n.removeCommunityQuestion),
|
|
content: Text(
|
|
context.l10n.removeCommunityConfirm(community.name),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(dialogContext).pop(false),
|
|
child: Text(context.l10n.cancel),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.of(dialogContext).pop(true),
|
|
style: FilledButton.styleFrom(backgroundColor: context.colors.error),
|
|
child: Text(context.l10n.remove),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed != true || !context.mounted) return;
|
|
|
|
final messenger = ScaffoldMessenger.of(context);
|
|
try {
|
|
await ref
|
|
.read(communityListProvider.notifier)
|
|
.removeCommunity(community.id);
|
|
if (closeSheetAfterRemoval && context.mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
} catch (e) {
|
|
messenger.showSnackBar(
|
|
SnackBar(content: Text(context.l10n.failedRemoveCommunity(e.toString()))),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CommunityIndicator extends ConsumerWidget {
|
|
final VoidCallback onTap;
|
|
|
|
const _CommunityIndicator({required this.onTap});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final activeAsync = ref.watch(activeCommunityProvider);
|
|
|
|
final activeCommunity = activeAsync.value;
|
|
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
behavior: HitTestBehavior.opaque,
|
|
child: _CommunityAvatar(
|
|
name: activeCommunity?.name,
|
|
relayUrl: activeCommunity?.relayUrl,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CommunityHeaderTitle extends ConsumerWidget {
|
|
final TextStyle? style;
|
|
final VoidCallback onTap;
|
|
|
|
const _CommunityHeaderTitle({required this.onTap, this.style});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final name = ref.watch(activeCommunityProvider).value?.name;
|
|
final title = name?.trim();
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: onTap,
|
|
child: SizedBox.expand(
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(left: Grid.xxs),
|
|
child: Text(
|
|
title == null || title.isEmpty ? context.l10n.community : title,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: style,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CommunityAvatar extends ConsumerWidget {
|
|
final String? name;
|
|
final String? relayUrl;
|
|
final double size;
|
|
|
|
const _CommunityAvatar({
|
|
super.key,
|
|
required this.name,
|
|
this.relayUrl,
|
|
this.size = _kTopSectionAvatarSize,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final trimmedName = name?.trim();
|
|
final initial = trimmedName != null && trimmedName.isNotEmpty
|
|
? trimmedName.substring(0, 1).toUpperCase()
|
|
: '?';
|
|
final relay = relayUrl;
|
|
final iconUrl = relay == null
|
|
? null
|
|
: ref.watch(communityIconProvider(relay)).value;
|
|
|
|
return AvatarImage(
|
|
imageUrl: iconUrl,
|
|
radius: size / 2,
|
|
backgroundColor: context.colors.primaryContainer,
|
|
fallback: Text(
|
|
initial,
|
|
style: context.textTheme.labelMedium?.copyWith(
|
|
color: context.colors.onPrimaryContainer,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|