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,501 @@
|
||||
part of '../rooms_panel.dart';
|
||||
|
||||
enum _SpaceMenuItem {
|
||||
newDm, createGroup, createRoom, joinRoom, discoverRooms,
|
||||
editSpace, adminSpace, leaveSpace,
|
||||
}
|
||||
|
||||
class _Header extends ConsumerWidget {
|
||||
final PyramidTheme pt;
|
||||
final String? spaceId;
|
||||
const _Header({required this.pt, required this.spaceId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isRealSpace = spaceId != null && spaceId != 'dms' && spaceId != 'rooms';
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final isMobile = size.width < 900 || size.height < 500;
|
||||
|
||||
final spaces = ref.watch(spacesProvider).valueOrNull ?? [];
|
||||
Room? activeSpace;
|
||||
if (isRealSpace) {
|
||||
try {
|
||||
activeSpace = spaces.firstWhere((s) => s.id == spaceId);
|
||||
} catch (_) {}
|
||||
}
|
||||
final client = ref.watch(matrixClientProvider).valueOrNull;
|
||||
final isAdmin =
|
||||
activeSpace != null && activeSpace.ownPowerLevel >= 50;
|
||||
|
||||
final name = isRealSpace
|
||||
? (activeSpace?.getLocalizedDisplayname() ?? 'Space')
|
||||
: (spaceId == 'dms' ? 'Chats' : 'Rooms');
|
||||
|
||||
void handleMenu(_SpaceMenuItem item) {
|
||||
switch (item) {
|
||||
case _SpaceMenuItem.newDm:
|
||||
showDialog(context: context, builder: (_) => const NewDmDialog());
|
||||
case _SpaceMenuItem.createGroup:
|
||||
showDialog(
|
||||
context: context, builder: (_) => const CreateJoinDialog());
|
||||
case _SpaceMenuItem.createRoom:
|
||||
showDialog(
|
||||
context: context, builder: (_) => const CreateJoinDialog());
|
||||
case _SpaceMenuItem.joinRoom:
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
const CreateJoinDialog(initialTab: CreateJoinTab.join));
|
||||
case _SpaceMenuItem.discoverRooms:
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
const CreateJoinDialog(initialTab: CreateJoinTab.discover));
|
||||
case _SpaceMenuItem.editSpace:
|
||||
if (activeSpace != null) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => SpaceEditDialog(space: activeSpace!));
|
||||
}
|
||||
case _SpaceMenuItem.adminSpace:
|
||||
if (activeSpace != null) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => SpaceAdminDialog(space: activeSpace!));
|
||||
}
|
||||
case _SpaceMenuItem.leaveSpace:
|
||||
if (activeSpace != null) {
|
||||
final space = activeSpace;
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: pt.bg2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rXl),
|
||||
side: BorderSide(color: pt.border)),
|
||||
title: Text('Space verlassen?',
|
||||
style: TextStyle(
|
||||
color: pt.fg,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700)),
|
||||
content: Text(
|
||||
'Du verlässt "${space.getLocalizedDisplayname()}".',
|
||||
style: TextStyle(color: pt.fgMuted, fontSize: 13)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text('Abbrechen',
|
||||
style: TextStyle(color: pt.fgMuted))),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: pt.danger,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8))),
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('Verlassen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((confirmed) {
|
||||
if (confirmed == true) {
|
||||
space.leave().then((_) {
|
||||
ref.read(activeSpaceIdProvider.notifier).state = 'rooms';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<PopupMenuEntry<_SpaceMenuItem>> buildMenuItems() {
|
||||
if (spaceId == 'dms' || spaceId == null) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: _SpaceMenuItem.newDm,
|
||||
child: _MenuRow(
|
||||
Icons.chat_bubble_outline_rounded, 'Neue Unterhaltung', pt)),
|
||||
PopupMenuItem(
|
||||
value: _SpaceMenuItem.createGroup,
|
||||
child: _MenuRow(Icons.group_add_rounded, 'Gruppe erstellen', pt)),
|
||||
];
|
||||
} else if (spaceId == 'rooms') {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: _SpaceMenuItem.createRoom,
|
||||
child: _MenuRow(Icons.add_rounded, 'Raum erstellen', pt)),
|
||||
PopupMenuItem(
|
||||
value: _SpaceMenuItem.joinRoom,
|
||||
child: _MenuRow(Icons.login_rounded, 'Beitreten', pt)),
|
||||
PopupMenuItem(
|
||||
value: _SpaceMenuItem.discoverRooms,
|
||||
child: _MenuRow(Icons.explore_rounded, 'Entdecken', pt)),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
if (isAdmin) ...[
|
||||
PopupMenuItem(
|
||||
value: _SpaceMenuItem.adminSpace,
|
||||
child: _MenuRow(
|
||||
Icons.admin_panel_settings_rounded, 'Space verwalten', pt)),
|
||||
PopupMenuItem(
|
||||
value: _SpaceMenuItem.editSpace,
|
||||
child: _MenuRow(Icons.edit_outlined, 'Name/Beschreibung', pt)),
|
||||
const PopupMenuDivider(),
|
||||
],
|
||||
PopupMenuItem(
|
||||
value: _SpaceMenuItem.leaveSpace,
|
||||
child: _MenuRow(Icons.exit_to_app_rounded, 'Space verlassen', pt,
|
||||
danger: true)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Check for banner — space banner for real spaces, server banner otherwise.
|
||||
final bannerOverrides = ref.watch(spaceBannerOverrideProvider);
|
||||
final serverBanners = ref.watch(serverBannerProvider).valueOrNull ?? {};
|
||||
String? bannerUrl;
|
||||
if (isRealSpace && activeSpace != null) {
|
||||
bannerUrl = bannerOverrides.containsKey(activeSpace.id)
|
||||
? bannerOverrides[activeSpace.id]
|
||||
: activeSpace.getState(_kSpaceBannerType)?.content['url'] as String?;
|
||||
} else if (client != null) {
|
||||
bannerUrl = serverBanners[client.homeserver.toString()];
|
||||
}
|
||||
final hasBanner = bannerUrl != null && client != null;
|
||||
|
||||
Widget controlsRow = Row(
|
||||
children: [
|
||||
if (isMobile)
|
||||
PyrIconBtn(
|
||||
icon: const Icon(Icons.menu_rounded),
|
||||
tooltip: 'Spaces',
|
||||
onPressed: () => Scaffold.of(context).openDrawer(),
|
||||
)
|
||||
else ...[
|
||||
PyrIconBtn(
|
||||
icon: const Icon(Icons.menu_rounded),
|
||||
tooltip: 'Toggle sidebar',
|
||||
onPressed: () =>
|
||||
ref.read(railCollapsedProvider.notifier).update((s) => !s),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
// Space avatar (only when no banner)
|
||||
if (isRealSpace && !hasBanner && client != null && activeSpace != null) ...[
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: pt.accent.withAlpha(40),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: activeSpace.avatar != null
|
||||
? MxcAvatar(
|
||||
mxcUri: activeSpace.avatar,
|
||||
client: client,
|
||||
size: 28,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
placeholder: (_) => Center(
|
||||
child: Text(
|
||||
name.isNotEmpty ? name[0].toUpperCase() : '?',
|
||||
style: TextStyle(
|
||||
color: pt.accent,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Center(
|
||||
child: Text(
|
||||
name.isNotEmpty ? name[0].toUpperCase() : '?',
|
||||
style: TextStyle(
|
||||
color: pt.accent,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: hasBanner ? Colors.white : pt.fg,
|
||||
fontSize: 15,
|
||||
fontWeight: isRealSpace ? FontWeight.w700 : FontWeight.w600,
|
||||
shadows: hasBanner
|
||||
? [const Shadow(blurRadius: 4, color: Colors.black45)]
|
||||
: null,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (isRealSpace && isAdmin)
|
||||
PyrIconBtn(
|
||||
icon: Icon(Icons.admin_panel_settings_rounded,
|
||||
color: hasBanner ? Colors.white70 : null),
|
||||
tooltip: 'Space verwalten',
|
||||
onPressed: () => handleMenu(_SpaceMenuItem.adminSpace),
|
||||
),
|
||||
if (!isRealSpace)
|
||||
_NotificationsButton(pt: pt, hasBanner: hasBanner),
|
||||
PopupMenuButton<_SpaceMenuItem>(
|
||||
tooltip: 'Erstellen',
|
||||
color: pt.bg2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(color: pt.border)),
|
||||
itemBuilder: (_) => buildMenuItems(),
|
||||
onSelected: handleMenu,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(Icons.add_rounded,
|
||||
color: hasBanner ? Colors.white70 : pt.fgDim, size: 18),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (hasBanner) {
|
||||
return SizedBox(
|
||||
height: 110,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Banner image
|
||||
MxcImage(
|
||||
mxcUri: bannerUrl,
|
||||
client: client,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: Container(color: pt.bg0),
|
||||
),
|
||||
// Gradient overlay — bottom-heavy so text is readable
|
||||
DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
stops: [0.0, 0.4, 1.0],
|
||||
colors: [
|
||||
Color(0x00000000),
|
||||
Color(0x55000000),
|
||||
Color(0xCC000000),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Controls at the bottom
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
child: controlsRow,
|
||||
),
|
||||
),
|
||||
// Bottom border
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(height: 1, color: pt.border.withAlpha(80)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: isRealSpace ? 56 : 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: isRealSpace ? pt.bg0 : pt.bg1,
|
||||
border: Border(bottom: BorderSide(color: pt.border)),
|
||||
),
|
||||
child: controlsRow,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MenuRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final PyramidTheme pt;
|
||||
final bool danger;
|
||||
const _MenuRow(this.icon, this.label, this.pt, {this.danger = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(children: [
|
||||
Icon(icon, size: 14, color: danger ? pt.danger : pt.fgMuted),
|
||||
const SizedBox(width: 8),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
color: danger ? pt.danger : pt.fg, fontSize: 13)),
|
||||
]);
|
||||
}
|
||||
|
||||
class _NotificationsButton extends ConsumerWidget {
|
||||
final PyramidTheme pt;
|
||||
final bool hasBanner;
|
||||
const _NotificationsButton({required this.pt, required this.hasBanner});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final rooms = ref.watch(roomListProvider).valueOrNull ?? [];
|
||||
final unread = rooms
|
||||
.where((r) =>
|
||||
r.membership == Membership.join &&
|
||||
(r.notificationCount > 0 || r.highlightCount > 0))
|
||||
.toList()
|
||||
..sort((a, b) {
|
||||
final hl = b.highlightCount.compareTo(a.highlightCount);
|
||||
if (hl != 0) return hl;
|
||||
return b.notificationCount.compareTo(a.notificationCount);
|
||||
});
|
||||
|
||||
return PopupMenuButton<String>(
|
||||
tooltip: 'Benachrichtigungen',
|
||||
color: pt.bg2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(color: pt.border)),
|
||||
offset: const Offset(0, 36),
|
||||
onSelected: (roomId) {
|
||||
ref.read(activeRoomIdProvider.notifier).state = roomId;
|
||||
ref.read(viewModeProvider.notifier).state = ViewMode.chat;
|
||||
},
|
||||
itemBuilder: (_) {
|
||||
if (unread.isEmpty) {
|
||||
return [
|
||||
PopupMenuItem<String>(
|
||||
enabled: false,
|
||||
child: Text('Keine neuen Benachrichtigungen',
|
||||
style: TextStyle(color: pt.fgDim, fontSize: 13)),
|
||||
),
|
||||
];
|
||||
}
|
||||
return unread
|
||||
.map((r) => PopupMenuItem<String>(
|
||||
value: r.id,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
r.highlightCount > 0
|
||||
? Icons.alternate_email_rounded
|
||||
: (r.isDirectChat
|
||||
? Icons.chat_bubble_outline_rounded
|
||||
: Icons.tag_rounded),
|
||||
size: 14,
|
||||
color:
|
||||
r.highlightCount > 0 ? pt.danger : pt.fgMuted,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
r.getLocalizedDisplayname(),
|
||||
style: TextStyle(color: pt.fg, fontSize: 13),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: r.highlightCount > 0 ? pt.danger : pt.accent,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
'${r.notificationCount > 0 ? r.notificationCount : r.highlightCount}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
.toList();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Icon(Icons.notifications_none_rounded,
|
||||
size: 18, color: hasBanner ? Colors.white70 : pt.fgDim),
|
||||
if (unread.isNotEmpty)
|
||||
Positioned(
|
||||
top: -1,
|
||||
right: -1,
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: pt.danger,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: pt.bg1, width: 1.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilterInput extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final PyramidTheme pt;
|
||||
final ValueChanged<String> onChanged;
|
||||
const _FilterInput(
|
||||
{required this.controller, required this.pt, required this.onChanged});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
||||
child: Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Icon(Icons.search_rounded, size: 14, color: pt.fgDim),
|
||||
),
|
||||
TextField(
|
||||
controller: controller,
|
||||
onChanged: onChanged,
|
||||
style: TextStyle(color: pt.fg, fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Filter rooms',
|
||||
hintStyle: TextStyle(color: pt.fgDim, fontSize: 13),
|
||||
contentPadding: const EdgeInsets.fromLTRB(28, 7, 10, 7),
|
||||
filled: true,
|
||||
fillColor: pt.bg2,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rSm),
|
||||
borderSide: BorderSide.none),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rSm),
|
||||
borderSide: const BorderSide(color: Colors.transparent)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rSm),
|
||||
borderSide: BorderSide(color: pt.accent, width: 1)),
|
||||
isDense: true,
|
||||
),
|
||||
cursorColor: pt.accent,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user