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>
502 lines
18 KiB
Dart
502 lines
18 KiB
Dart
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,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|