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( 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> 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( 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( enabled: false, child: Text('Keine neuen Benachrichtigungen', style: TextStyle(color: pt.fgDim, fontSize: 13)), ), ]; } return unread .map((r) => PopupMenuItem( 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 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, ), ], ), ); } }