a863fa3b9a
Gleiche Technik wie bei settings_modal.dart/message_group.dart: reine Verschiebung per Dart part/part-of, keine Logikänderung. rooms_panel.dart schrumpft von 2555 auf 31 Zeilen (nur noch Bibliothekskopf). Verifiziert per automatisiertem Blockvergleich gegen das Original und flutter analyze. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
280 lines
8.3 KiB
Dart
280 lines
8.3 KiB
Dart
part of '../rooms_panel.dart';
|
|
|
|
class _DmAvatar extends ConsumerStatefulWidget {
|
|
final Room room;
|
|
final PyramidTheme pt;
|
|
const _DmAvatar({required this.room, required this.pt});
|
|
|
|
@override
|
|
ConsumerState<_DmAvatar> createState() => _DmAvatarState();
|
|
}
|
|
|
|
class _DmAvatarState extends ConsumerState<_DmAvatar> {
|
|
CachedPresence? _presence;
|
|
StreamSubscription<CachedPresence>? _sub;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadPresence();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_sub?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadPresence() async {
|
|
final client = ref.read(matrixClientProvider).valueOrNull;
|
|
if (client == null) return;
|
|
final userId = widget.room.directChatMatrixID;
|
|
if (userId == null) return;
|
|
try {
|
|
final p = await client.fetchCurrentPresence(userId);
|
|
if (mounted) setState(() => _presence = p);
|
|
} catch (_) {}
|
|
_sub = client.onPresenceChanged.stream
|
|
.where((p) => p.userid == userId)
|
|
.listen((p) {
|
|
if (mounted) setState(() => _presence = p);
|
|
});
|
|
}
|
|
|
|
Color? _dotColor() {
|
|
final p = _presence;
|
|
if (p == null) return null;
|
|
// Server-Presence kann veraltet sein (z.B. nie als offline gemeldet).
|
|
// Nur als online werten, wenn die letzte Aktivität wirklich frisch ist.
|
|
final last = p.lastActiveTimestamp;
|
|
final fresh = last != null &&
|
|
DateTime.now().difference(last) < const Duration(minutes: 5);
|
|
if (p.presence == PresenceType.online &&
|
|
(p.currentlyActive == true || fresh)) {
|
|
return PyramidColors.online;
|
|
}
|
|
if (p.presence == PresenceType.unavailable) return PyramidColors.away;
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final name = widget.room.getLocalizedDisplayname();
|
|
final initial = name.isNotEmpty ? name[0].toUpperCase() : '?';
|
|
final color = _colorFromName(name);
|
|
final client = ref.watch(matrixClientProvider).valueOrNull;
|
|
|
|
Widget avatar;
|
|
if (client != null && widget.room.avatar != null) {
|
|
avatar = MxcAvatar(
|
|
mxcUri: widget.room.avatar,
|
|
client: client,
|
|
size: 22,
|
|
borderRadius: BorderRadius.circular(11),
|
|
placeholder: (_) =>
|
|
_InitialCircle(initial: initial, color: color, size: 22),
|
|
);
|
|
} else {
|
|
avatar = _InitialCircle(initial: initial, color: color, size: 22);
|
|
}
|
|
|
|
final dot = _dotColor();
|
|
if (dot == null) return avatar;
|
|
return Stack(clipBehavior: Clip.none, children: [
|
|
avatar,
|
|
Positioned(
|
|
right: -2,
|
|
bottom: -2,
|
|
child: Container(
|
|
width: 9,
|
|
height: 9,
|
|
decoration: BoxDecoration(
|
|
color: dot,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: widget.pt.bg1, width: 1.5),
|
|
),
|
|
),
|
|
),
|
|
]);
|
|
}
|
|
|
|
Color _colorFromName(String name) {
|
|
const colors = [
|
|
Color(0xFF06B6D4), Color(0xFFA855F7), Color(0xFF10B981),
|
|
Color(0xFFF43F5E), Color(0xFF3B82F6), Color(0xFFF59E0B),
|
|
];
|
|
if (name.isEmpty) return colors[0];
|
|
return colors[name.codeUnitAt(0) % colors.length];
|
|
}
|
|
}
|
|
|
|
class _InitialCircle extends StatelessWidget {
|
|
final String initial;
|
|
final Color color;
|
|
final double size;
|
|
const _InitialCircle(
|
|
{required this.initial, required this.color, required this.size});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: size,
|
|
height: size,
|
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
|
child: Center(
|
|
child: Text(initial,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w600))),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RoomMenuBtn extends StatefulWidget {
|
|
final PyramidTheme pt;
|
|
final VoidCallback onInvite;
|
|
final VoidCallback onSettings;
|
|
final VoidCallback onLeave;
|
|
const _RoomMenuBtn({required this.pt, required this.onInvite, required this.onSettings, required this.onLeave});
|
|
|
|
@override
|
|
State<_RoomMenuBtn> createState() => _RoomMenuBtnState();
|
|
}
|
|
|
|
class _RoomMenuBtnState extends State<_RoomMenuBtn> {
|
|
bool _hovered = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PopupMenuButton<String>(
|
|
color: widget.pt.bg2,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
side: BorderSide(color: widget.pt.border)),
|
|
offset: const Offset(-100, 24),
|
|
tooltip: '',
|
|
onSelected: (v) {
|
|
if (v == 'invite') widget.onInvite();
|
|
if (v == 'settings') widget.onSettings();
|
|
if (v == 'leave') widget.onLeave();
|
|
},
|
|
itemBuilder: (_) => [
|
|
PopupMenuItem(
|
|
value: 'invite',
|
|
height: 36,
|
|
child: Row(children: [
|
|
Icon(Icons.person_add_outlined, size: 14, color: widget.pt.fgMuted),
|
|
const SizedBox(width: 8),
|
|
Text('Nutzer einladen', style: TextStyle(color: widget.pt.fg, fontSize: 13)),
|
|
]),
|
|
),
|
|
PopupMenuItem(
|
|
value: 'settings',
|
|
height: 36,
|
|
child: Row(children: [
|
|
Icon(Icons.settings_rounded, size: 14, color: widget.pt.fgMuted),
|
|
const SizedBox(width: 8),
|
|
Text('Einstellungen', style: TextStyle(color: widget.pt.fg, fontSize: 13)),
|
|
]),
|
|
),
|
|
PopupMenuDivider(height: 1),
|
|
PopupMenuItem(
|
|
value: 'leave',
|
|
height: 36,
|
|
child: Row(children: [
|
|
Icon(Icons.exit_to_app_rounded, size: 14, color: widget.pt.danger),
|
|
const SizedBox(width: 8),
|
|
Text('Raum verlassen', style: TextStyle(color: widget.pt.danger, fontSize: 13)),
|
|
]),
|
|
),
|
|
],
|
|
child: MouseRegion(
|
|
onEnter: (_) => setState(() => _hovered = true),
|
|
onExit: (_) => setState(() => _hovered = false),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 120),
|
|
width: 20,
|
|
height: 20,
|
|
decoration: BoxDecoration(
|
|
color: _hovered ? widget.pt.bgActive : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Center(
|
|
child: Icon(Icons.more_horiz_rounded,
|
|
size: 12,
|
|
color: _hovered ? widget.pt.fg : widget.pt.fgDim)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _HoverAction extends StatefulWidget {
|
|
final IconData icon;
|
|
final PyramidTheme pt;
|
|
final VoidCallback? onTap;
|
|
const _HoverAction({required this.icon, required this.pt, this.onTap});
|
|
|
|
@override
|
|
State<_HoverAction> createState() => _HoverActionState();
|
|
}
|
|
|
|
class _HoverActionState extends State<_HoverAction> {
|
|
bool _hovered = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: widget.onTap,
|
|
child: MouseRegion(
|
|
cursor: widget.onTap != null
|
|
? SystemMouseCursors.click
|
|
: MouseCursor.defer,
|
|
onEnter: (_) => setState(() => _hovered = true),
|
|
onExit: (_) => setState(() => _hovered = false),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 120),
|
|
width: 20,
|
|
height: 20,
|
|
decoration: BoxDecoration(
|
|
color: _hovered ? widget.pt.bgActive : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Center(
|
|
child: Icon(widget.icon,
|
|
size: 12,
|
|
color: _hovered ? widget.pt.fg : widget.pt.fgDim)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _UnreadBadge extends StatelessWidget {
|
|
final int count;
|
|
final bool isMention;
|
|
final PyramidTheme pt;
|
|
const _UnreadBadge(
|
|
{required this.count, required this.isMention, required this.pt});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 5),
|
|
constraints: const BoxConstraints(minWidth: 18, minHeight: 18),
|
|
decoration: BoxDecoration(
|
|
color: isMention ? pt.danger : pt.accent,
|
|
borderRadius: BorderRadius.circular(999),
|
|
),
|
|
child: Center(
|
|
child: Text(count > 99 ? '99+' : '$count',
|
|
style: TextStyle(
|
|
color: isMention ? Colors.white : pt.accentFg,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w700))),
|
|
);
|
|
}
|
|
}
|