refactor: Gott-Datei rooms_panel.dart in 8 Dateien aufgeteilt
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>
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
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))),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user