chore: restore product scaffold to rollback baseline

This commit is contained in:
Edison Jwa
2026-05-29 14:02:04 +09:00
parent 2896f14ec9
commit fe6e07353e
434 changed files with 27278 additions and 63230 deletions
+188 -176
View File
@@ -1,3 +1,5 @@
import 'dart:async' show unawaited;
import 'package:flutter/material.dart';
import '../l10n/generated/app_localizations.dart';
@@ -9,12 +11,9 @@ import '../src/rust/api.dart' as rust;
import 'bbcode_text.dart';
const double _chatSidebarTileExtent = 92;
const double _chatSidebarIndicatorExtent = 76;
const double _chatSidebarIconExtent = 24;
const double _chatSidebarIconSize = 20;
const BorderRadius _chatSidebarIndicatorRadius = BorderRadius.all(
Radius.circular(20),
);
const double _chatSidebarCompactTileExtent = 76;
const double _chatSidebarCompactHeight = 84;
const double _chatMobileBreakpoint = 600;
/// One chat/activity message shown in the chat hub.
class ChatEntry {
@@ -616,12 +615,22 @@ class _ChatPageState extends State<ChatPage> {
final messages = _messages;
final currentChannelId = _currentChannelId;
final channelName = snapshotChannelName(snapshot, currentChannelId);
final l10n = AppL10n.of(context);
final detail = _ChatDetailView(
target: _selectedTarget,
clientName: _selectedClientName,
snapshot: snapshot,
messages: messages,
currentChannelId: currentChannelId,
channelName: channelName,
onTs3ServerLink: widget.onTs3ServerLink,
);
return Scaffold(
appBar: AppBar(
title: Text('Chat — ${snapshot.serverName}'),
title: Text('${l10n.chatAction} - ${snapshot.serverName}'),
actions: [
IconButton(
tooltip: 'Close chat',
tooltip: l10n.chatCloseAction,
icon: const Icon(Icons.close),
onPressed: _selectedPrivateClientId == null
? null
@@ -629,9 +638,10 @@ class _ChatPageState extends State<ChatPage> {
),
],
),
body: Row(
children: [
_ChatSidebar(
body: LayoutBuilder(
builder: (context, constraints) {
final sidebar = _ChatSidebar(
compact: constraints.maxWidth < _chatMobileBreakpoint,
selectedTarget: _selectedTarget,
privateChats: _privateChats,
onSelect: _selectTarget,
@@ -639,20 +649,24 @@ class _ChatPageState extends State<ChatPage> {
_closedPrivateChats.remove(id);
_selectTarget(rust.BridgeMessageTarget.client(id), name: name);
}),
),
const VerticalDivider(width: 1),
Expanded(
child: _ChatDetailView(
target: _selectedTarget,
clientName: _selectedClientName,
snapshot: snapshot,
messages: messages,
currentChannelId: currentChannelId,
channelName: channelName,
onTs3ServerLink: widget.onTs3ServerLink,
),
),
],
);
if (constraints.maxWidth < _chatMobileBreakpoint) {
return Column(
children: [
sidebar,
const Divider(height: 1),
Expanded(child: detail),
],
);
}
return Row(
children: [
sidebar,
const VerticalDivider(width: 1),
Expanded(child: detail),
],
);
},
),
);
}
@@ -740,12 +754,14 @@ class _PrivateChatItem {
class _ChatSidebar extends StatelessWidget {
const _ChatSidebar({
required this.compact,
required this.selectedTarget,
required this.privateChats,
required this.onSelect,
required this.onNewPrivateChat,
});
final bool compact;
final rust.BridgeMessageTarget selectedTarget;
final List<_PrivateChatItem> privateChats;
final void Function(rust.BridgeMessageTarget target, {String name}) onSelect;
@@ -753,64 +769,85 @@ class _ChatSidebar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final dividerColor = theme.colorScheme.outlineVariant.withValues(
alpha: 0.45,
final l10n = AppL10n.of(context);
final fixedItems = [
_ChatSidebarItem(
compact: compact,
icon: Icons.dns_outlined,
label: 'Server',
selected: selectedTarget is rust.BridgeMessageTarget_Server,
onTap: () => onSelect(const rust.BridgeMessageTarget.server()),
),
_ChatSidebarItem(
compact: compact,
icon: Icons.tag,
label: 'Channel',
selected: selectedTarget is rust.BridgeMessageTarget_Channel,
onTap: () => onSelect(const rust.BridgeMessageTarget.channel()),
),
];
final privateItems = [
for (final chat in privateChats)
_ChatSidebarItem(
compact: compact,
icon: Icons.person_outline,
label: chat.name.isNotEmpty ? chat.name : 'Direct',
selected: switch (selectedTarget) {
rust.BridgeMessageTarget_Client(:final field0) => field0 == chat.id,
_ => false,
},
onTap: () => onSelect(
rust.BridgeMessageTarget.client(chat.id),
name: chat.name,
),
),
];
final addButton = Padding(
padding: EdgeInsets.all(compact ? 4 : 8),
child: IconButton.filledTonal(
tooltip: l10n.chatNewPrivateAction,
icon: const Icon(Icons.add),
onPressed: onNewPrivateChat,
),
);
return SizedBox(
width: _chatSidebarTileExtent,
child: Material(
color: theme.colorScheme.surfaceContainerLow,
child: Column(
final compactDivider = ColoredBox(
color: Theme.of(context).dividerColor,
child: const SizedBox(width: 1, height: double.infinity),
);
if (compact) {
return SizedBox(
height: _chatSidebarCompactHeight,
child: Row(
children: [
const SizedBox(height: 8),
_ChatSidebarItem(
icon: Icons.dns_outlined,
label: 'Server',
selected: selectedTarget is rust.BridgeMessageTarget_Server,
onTap: () => onSelect(const rust.BridgeMessageTarget.server()),
),
_ChatSidebarItem(
icon: Icons.tag,
label: 'Channel',
selected: selectedTarget is rust.BridgeMessageTarget_Channel,
onTap: () => onSelect(const rust.BridgeMessageTarget.channel()),
),
Divider(height: 1, indent: 12, endIndent: 12, color: dividerColor),
...fixedItems,
compactDivider,
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 6),
itemCount: privateChats.length,
itemBuilder: (context, index) {
final chat = privateChats[index];
final selected = switch (selectedTarget) {
rust.BridgeMessageTarget_Client(:final field0) =>
field0 == chat.id,
_ => false,
};
return _ChatSidebarItem(
icon: Icons.person_outline,
label: chat.name.isNotEmpty ? chat.name : 'Direct',
selected: selected,
onTap: () => onSelect(
rust.BridgeMessageTarget.client(chat.id),
name: chat.name,
),
);
},
),
),
Divider(height: 1, indent: 12, endIndent: 12, color: dividerColor),
Padding(
padding: const EdgeInsets.fromLTRB(8, 10, 8, 12),
child: IconButton.filledTonal(
tooltip: 'New private chat',
icon: const Icon(Icons.add),
onPressed: onNewPrivateChat,
child: ListView(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero,
children: privateItems,
),
),
compactDivider,
addButton,
],
),
);
}
return SizedBox(
width: _chatSidebarTileExtent,
child: Column(
children: [
...fixedItems,
const Divider(height: 1),
Expanded(
child: ListView(padding: EdgeInsets.zero, children: privateItems),
),
const Divider(height: 1),
addButton,
],
),
);
}
@@ -818,12 +855,14 @@ class _ChatSidebar extends StatelessWidget {
class _ChatSidebarItem extends StatelessWidget {
const _ChatSidebarItem({
required this.compact,
required this.icon,
required this.label,
required this.selected,
required this.onTap,
});
final bool compact;
final IconData icon;
final String label;
final bool selected;
@@ -832,63 +871,57 @@ class _ChatSidebarItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scheme = theme.colorScheme;
final indicatorColor = selected
? scheme.primaryContainer
: Colors.transparent;
final contentColor = selected
? scheme.onPrimaryContainer
: scheme.onSurfaceVariant;
final labelStyle = theme.textTheme.labelSmall?.copyWith(
color: contentColor,
height: 1.15,
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
);
return SizedBox(
width: _chatSidebarTileExtent,
height: _chatSidebarTileExtent,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: _chatSidebarIndicatorRadius,
child: Center(
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.easeOutCubic,
width: _chatSidebarIndicatorExtent,
height: _chatSidebarIndicatorExtent,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
decoration: BoxDecoration(
color: indicatorColor,
borderRadius: _chatSidebarIndicatorRadius,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: _chatSidebarIconExtent,
height: _chatSidebarIconExtent,
child: Icon(
icon,
size: _chatSidebarIconSize,
color: contentColor,
),
final colorScheme = theme.colorScheme;
final fg = selected
? colorScheme.onSecondaryContainer
: colorScheme.onSurface;
final tileExtent = compact
? _chatSidebarCompactTileExtent
: _chatSidebarTileExtent;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Ink(
width: tileExtent,
height: compact ? 72 : tileExtent,
decoration: BoxDecoration(
color: selected
? colorScheme.secondaryContainer
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: compact ? 6 : 8,
vertical: compact ? 8 : 10,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 24,
height: 24,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [Icon(icon, size: 18, color: fg)],
),
const SizedBox(height: 8),
SizedBox(
width: _chatSidebarIndicatorExtent - 16,
child: Text(
label,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: labelStyle,
),
),
const SizedBox(height: 8),
Text(
label,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: theme.textTheme.labelSmall?.copyWith(
color: fg,
height: 1.1,
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
),
],
),
),
],
),
),
),
@@ -926,6 +959,7 @@ class _ClientPickerDialogState extends State<_ClientPickerDialog> {
@override
Widget build(BuildContext context) {
final l10n = AppL10n.of(context);
final groups = filterChatClientPickerGroups(
channels: widget.channels,
clientsByChannel: widget.byChannel,
@@ -943,10 +977,10 @@ class _ClientPickerDialogState extends State<_ClientPickerDialog> {
padding: const EdgeInsets.fromLTRB(20, 20, 20, 8),
child: TextField(
controller: _searchCtl,
decoration: const InputDecoration(
hintText: 'Search clients...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
decoration: InputDecoration(
hintText: l10n.chatSearchClientsHint,
prefixIcon: const Icon(Icons.search),
border: const OutlineInputBorder(),
isDense: true,
),
onChanged: (v) => setState(() => _query = v),
@@ -1044,7 +1078,6 @@ class _ChatDetailViewState extends State<_ChatDetailView> {
final _scrollCtl = ScrollController();
int _lastRenderedMessageCount = -1;
rust.BridgeMessageTarget? _lastRenderedTarget;
bool _sending = false;
Iterable<ChatEntry> get _filtered {
if (widget.target is rust.BridgeMessageTarget_Channel) {
@@ -1074,51 +1107,30 @@ class _ChatDetailViewState extends State<_ChatDetailView> {
super.dispose();
}
Future<void> _send() async {
void _send() {
final text = _textCtl.text.trim();
if (text.isEmpty || !_canSend || _sending) return;
if (text.isEmpty || !_canSend) return;
_textCtl.clear();
setState(() => _sending = true);
try {
await rust.sendChatMessage(message: text, target: widget.target);
if (!mounted) return;
final ownId = widget.snapshot.ownClientId;
setState(() {
widget.messages.add(
ChatEntry(
senderId: ownId,
senderName: widget.target is rust.BridgeMessageTarget_Poke
? widget.clientName
: 'You',
message: text,
target: widget.target,
isSelf: true,
timestamp: DateTime.now(),
),
);
if (widget.messages.length > 200) {
widget.messages.removeRange(0, widget.messages.length - 200);
}
});
_scrollToBottom();
} catch (error) {
if (!mounted) return;
_textCtl.text = text;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
content: Text(
'Could not send message: $error',
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
unawaited(rust.sendChatMessage(message: text, target: widget.target));
final ownId = widget.snapshot.ownClientId;
setState(() {
widget.messages.add(
ChatEntry(
senderId: ownId,
senderName: widget.target is rust.BridgeMessageTarget_Poke
? widget.clientName
: 'You',
message: text,
target: widget.target,
isSelf: true,
timestamp: DateTime.now(),
),
);
} finally {
if (mounted) {
setState(() => _sending = false);
if (widget.messages.length > 200) {
widget.messages.removeRange(0, widget.messages.length - 200);
}
}
});
_scrollToBottom();
}
void _scrollToBottom() {