import 'package:flutter/material.dart'; import '../../api/oa_client.dart'; import '../../session/session.dart'; import '../../widgets/auth_image.dart'; import '../desk_theme.dart'; class DeskNavRail extends StatelessWidget { const DeskNavRail({ super.key, required this.session, required this.api, required this.index, required this.onChanged, required this.items, }); final SessionStore session; final OaClient api; final int index; final ValueChanged onChanged; final List<(IconData, IconData, String, int)> items; @override Widget build(BuildContext context) { return ColoredBox( color: kDeskRail, child: SizedBox( width: kDeskRailWidth, child: Column( children: [ const SizedBox(height: 18), DeskAvatar( label: session.displayName, fileId: '${session.user['avatarFileId'] ?? ''}', api: api, size: 36, ), const SizedBox(height: 18), Expanded( child: ListView( padding: EdgeInsets.zero, children: [ for (var i = 0; i < items.length; i++) _RailItem( active: index == i, icon: index == i ? items[i].$2 : items[i].$1, label: items[i].$3, badge: items[i].$4, onTap: () => onChanged(i), ), ], ), ), const SizedBox(height: 12), ], ), ), ); } } class _RailItem extends StatelessWidget { const _RailItem({ required this.active, required this.icon, required this.label, required this.badge, required this.onTap, }); final bool active; final IconData icon; final String label; final int badge; final VoidCallback onTap; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), child: Material( color: active ? kDeskActive : Colors.transparent, borderRadius: BorderRadius.circular(8), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( children: [ Stack( clipBehavior: Clip.none, children: [ Icon(icon, size: 22, color: active ? kDeskBind : const Color(0xFF5C5C5C)), if (badge > 0) Positioned( right: -10, top: -6, child: Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), constraints: const BoxConstraints(minWidth: 16, minHeight: 16), decoration: BoxDecoration( color: const Color(0xFFFA5151), borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, child: Text( badge > 99 ? '99+' : '$badge', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.w600), ), ), ), ], ), const SizedBox(height: 3), Text( label, style: TextStyle( fontSize: 11, color: active ? kDeskBind : const Color(0xFF5C5C5C), fontWeight: active ? FontWeight.w600 : FontWeight.w400, ), ), ], ), ), ), ), ); } } class DeskAvatar extends StatelessWidget { const DeskAvatar({ super.key, required this.label, required this.api, this.fileId, this.size = 40, this.color, this.icon, }); final String label; final OaClient api; final String? fileId; final double size; final Color? color; final IconData? icon; @override Widget build(BuildContext context) { final id = fileId ?? ''; if (id.isNotEmpty) { return ClipRRect( borderRadius: BorderRadius.circular(6), child: AuthImage(fileId: id, api: api, width: size, height: size, fit: BoxFit.cover), ); } final c = color ?? kDeskBind; final ch = label.isNotEmpty ? label.characters.first : '?'; return Container( width: size, height: size, decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(6)), alignment: Alignment.center, child: icon != null ? Icon(icon, color: Colors.white, size: size * 0.5) : Text(ch, style: TextStyle(color: Colors.white, fontSize: size * 0.38, fontWeight: FontWeight.w600)), ); } } class DeskPaneHeader extends StatelessWidget { const DeskPaneHeader({ super.key, required this.title, this.actions = const [], this.bottom, this.leading, }); final String title; final List actions; final Widget? bottom; final Widget? leading; @override Widget build(BuildContext context) { return ColoredBox( color: kDeskPane, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( height: 48, child: Row( children: [ if (leading != null) leading!, Padding( padding: EdgeInsets.only(left: leading == null ? 14 : 4), child: Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: kDeskInk)), ), const Spacer(), ...actions, const SizedBox(width: 6), ], ), ), if (bottom != null) bottom!, const Divider(height: 1, color: kDeskLine), ], ), ); } } class DeskSearchField extends StatelessWidget { const DeskSearchField({super.key, required this.hint, this.onChanged, this.controller}); final String hint; final ValueChanged? onChanged; final TextEditingController? controller; @override Widget build(BuildContext context) { return Container( height: 32, decoration: BoxDecoration(color: const Color(0xFFEFEFEF), borderRadius: BorderRadius.circular(6)), padding: const EdgeInsets.symmetric(horizontal: 10), child: Row( children: [ const Icon(Icons.search, size: 16, color: kDeskMute), const SizedBox(width: 6), Expanded( child: TextField( controller: controller, onChanged: onChanged, style: const TextStyle(fontSize: 13), decoration: InputDecoration( hintText: hint, isDense: true, border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, contentPadding: EdgeInsets.zero, hintStyle: const TextStyle(color: Color(0xFFB2B2B2), fontSize: 13), ), ), ), ], ), ); } } class DeskConvRow extends StatelessWidget { const DeskConvRow({ super.key, required this.name, required this.preview, required this.time, required this.unread, required this.selected, required this.onTap, this.avatarId, this.api, this.muted = false, this.pinned = false, this.onSecondaryTap, }); final String name; final String preview; final String time; final int unread; final bool selected; final VoidCallback onTap; final String? avatarId; final OaClient? api; final bool muted; final bool pinned; final void Function(TapDownDetails)? onSecondaryTap; @override Widget build(BuildContext context) { return Material( color: selected ? kDeskActive : Colors.transparent, child: InkWell( onTap: onTap, onSecondaryTapDown: onSecondaryTap, child: Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), child: Row( children: [ if (api != null) DeskAvatar(label: name, fileId: avatarId, api: api!, size: 40) else DeskAvatar(label: name, api: OaClient(SessionStore()), size: 40), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ if (pinned) const Padding( padding: EdgeInsets.only(right: 4), child: Icon(Icons.push_pin, size: 12, color: kDeskMute), ), Expanded( child: Text( name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: kDeskInk), ), ), Text(time, style: const TextStyle(fontSize: 11, color: kDeskMute)), ], ), const SizedBox(height: 4), Row( children: [ Expanded( child: Text( preview, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: muted ? kDeskMute : const Color(0xFF9A9A9A)), ), ), if (muted) const Icon(Icons.notifications_off_outlined, size: 14, color: kDeskMute), if (unread > 0) ...[ const SizedBox(width: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), decoration: BoxDecoration( color: const Color(0xFFFA5151), borderRadius: BorderRadius.circular(9), ), child: Text( unread > 99 ? '99+' : '$unread', style: const TextStyle(color: Colors.white, fontSize: 10), ), ), ], ], ), ], ), ), ], ), ), ), ); } } class DeskEmptyChat extends StatelessWidget { const DeskEmptyChat({super.key, this.hint = '选择一个会话开始聊天'}); final String hint; @override Widget build(BuildContext context) { return ColoredBox( color: const Color(0xFFF5F5F5), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Opacity( opacity: 0.1, child: Container( width: 100, height: 100, decoration: BoxDecoration(color: kDeskBind, borderRadius: BorderRadius.circular(24)), child: const Icon(Icons.chat_bubble_outline, size: 52, color: Colors.white), ), ), const SizedBox(height: 16), Text(hint, style: const TextStyle(color: kDeskMute, fontSize: 13)), ], ), ), ); } } class DeskTabStrip extends StatelessWidget { const DeskTabStrip({ super.key, required this.tabs, required this.active, required this.onSelect, required this.onClose, }); final List<(String key, String title, IconData icon, Color color)> tabs; final int active; final ValueChanged onSelect; final ValueChanged onClose; @override Widget build(BuildContext context) { return ColoredBox( color: kDeskPane, child: Column( children: [ SizedBox( height: 40, child: ListView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 8), children: [ for (var i = 0; i < tabs.length; i++) Padding( padding: const EdgeInsets.only(right: 2, top: 6, bottom: 6), child: Material( color: active == i ? const Color(0xFFF0F4FA) : Colors.transparent, borderRadius: BorderRadius.circular(6), child: InkWell( onTap: () => onSelect(i), borderRadius: BorderRadius.circular(6), child: Padding( padding: EdgeInsets.fromLTRB(10, 5, i > 0 ? 4 : 10, 5), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(tabs[i].$3, size: 14, color: tabs[i].$4), const SizedBox(width: 5), Text( tabs[i].$2, style: TextStyle( fontSize: 12, color: active == i ? kDeskInk : kDeskMute, fontWeight: active == i ? FontWeight.w600 : FontWeight.w400, ), ), if (i > 0) ...[ const SizedBox(width: 2), InkWell( onTap: () => onClose(i), borderRadius: BorderRadius.circular(8), child: const Padding( padding: EdgeInsets.all(2), child: Icon(Icons.close, size: 12, color: kDeskMute), ), ), ], ], ), ), ), ), ), ], ), ), const Divider(height: 1, color: kDeskLine), ], ), ); } } class DeskAppCard extends StatelessWidget { const DeskAppCard({ super.key, required this.title, required this.desc, required this.icon, required this.color, required this.onTap, }); final String title; final String desc; final IconData icon; final Color color; final VoidCallback onTap; @override Widget build(BuildContext context) { return Material( color: Colors.white, borderRadius: BorderRadius.circular(8), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.fromLTRB(14, 14, 14, 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 42, height: 42, decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: Colors.white, size: 22), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: kDeskInk)), const SizedBox(height: 4), Text(desc, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: kDeskMute, height: 1.35)), ], ), ), ], ), ), ), ); } } class DeskFilterChips extends StatelessWidget { const DeskFilterChips({super.key, required this.value, required this.onChanged, required this.items}); final String value; final ValueChanged onChanged; final List<(String, String)> items; @override Widget build(BuildContext context) { return Row( children: [ for (final it in items) ...[ _chip(it.$1, it.$2), if (it != items.last) const SizedBox(width: 6), ], ], ); } Widget _chip(String id, String label) { final on = value == id; return GestureDetector( onTap: () => onChanged(id), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: on ? kDeskActive : Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: on ? const Color(0xFFB8D4FF) : kDeskLine), ), child: Text(label, style: TextStyle(fontSize: 12, color: on ? kDeskBind : kDeskMute, fontWeight: on ? FontWeight.w600 : FontWeight.w400)), ), ); } } void deskToast(BuildContext context, String msg, {bool error = false}) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(msg), backgroundColor: error ? const Color(0xFFFA5151) : null, behavior: SnackBarBehavior.floating, width: 320, ), ); }