fix(ui): UX-Audit – Sackgassen, tote Knöpfe, englische Reste; Screenshot-Tour
Bernds Wunsch 2026-10-08: UI grundsätzlich prüfen (Navigation, Sackgassen, Anruf-Logik). Audit + Konzept in docs/UI_AUDIT.md, belegt mit neuer Screenshot-Tour integration_test/ui_audit_tour_test.dart (35 Zustände, Handy + PC, zwei Testkonten, Test-Space mit Sprachkanal). Sofort behoben: - S1/S2: Gegenseite legt auf / Anruf scheitert / Sprachkanal bricht ab → automatisch zurück zum Chat mit Hinweis (Fehler sichtbar statt still); leere Anrufansicht deutsch mit Zurück-Knopf (vorher englische Sackgasse ohne Zurück am Handy). - S4: toter „Threads“-Knopf entfernt. S5: Space betreten (PC) öffnet letzten bzw. ersten Textkanal. S6: keine doppelte Anrufsteuerung am PC. - Annehmen-Fenster mit Anrufer-Avatar und Anrufart; eigene Kachel mit eigenem Initial statt „D“; Mitgliederliste rund mit Profilbild und „(Du)“; Tastaturkürzel nur am PC; englische Texte in Anruf/Leerzuständen deutsch. - Tests: Fehlerempfänger des Testrahmens nach app.main() zurück (vorher verschwanden Fehlermeldungen), Lesebestätigungs-Test im Wegwerf-Raum statt in der DM, robustere Schritte; test.ps1 räumt liegengebliebene Test-Prozesse weg; gemeinsame Hilfen integration_test/support/. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
842dd61031
commit
87b472a373
@@ -0,0 +1,8 @@
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Raumtyp der Pyramid-Sprachkanäle (creation_content.type).
|
||||
const voiceChannelRoomType = 'io.pyramid.voice_channel';
|
||||
|
||||
/// Ist [room] ein Sprachkanal (statt eines Text-Chats)?
|
||||
bool isVoiceChannelRoom(Room room) =>
|
||||
room.getState(EventTypes.RoomCreate)?.content['type'] == voiceChannelRoomType;
|
||||
@@ -49,7 +49,7 @@ class _MiniCallWidgetState extends ConsumerState<MiniCallWidget> {
|
||||
final call = widget.call;
|
||||
|
||||
final String displayName = isVoip
|
||||
? (call.currentCall?.room.getLocalizedDisplayname() ?? 'Unknown')
|
||||
? (call.currentCall?.room.getLocalizedDisplayname() ?? 'Unbekannt')
|
||||
: call.displayName;
|
||||
|
||||
final bool isConnecting = isVoip ? false : call.isConnecting;
|
||||
@@ -160,7 +160,7 @@ class _MiniCallWidgetState extends ConsumerState<MiniCallWidget> {
|
||||
const _LiveDot(),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
isConnecting ? 'CONNECTING...' : 'VOICE CONNECTED',
|
||||
isConnecting ? 'CONNECTING...' : (isVoip ? 'ANRUF LÄUFT' : (call.isVoiceChannel ? 'SPRACHKANAL' : 'GRUPPENANRUF')),
|
||||
style: TextStyle(
|
||||
color: pt.online,
|
||||
fontSize: 10,
|
||||
|
||||
@@ -14,7 +14,7 @@ class _BigAvatar extends StatelessWidget {
|
||||
gradient: LinearGradient(colors: [data.avatarColor, data.avatarColor.withAlpha(150)], begin: Alignment.topLeft, end: Alignment.bottomRight),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(child: Text(data.name.isNotEmpty ? data.name[0].toUpperCase() : '?', style: const TextStyle(color: Colors.white, fontSize: 40, fontWeight: FontWeight.bold))),
|
||||
child: Center(child: Text(data.initial, style: const TextStyle(color: Colors.white, fontSize: 40, fontWeight: FontWeight.bold))),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class _SmallAvatar extends StatelessWidget {
|
||||
gradient: LinearGradient(colors: [data.avatarColor, data.avatarColor.withAlpha(150)], begin: Alignment.topLeft, end: Alignment.bottomRight),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(child: Text(data.name.isNotEmpty ? data.name[0].toUpperCase() : '?', style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold))),
|
||||
child: Center(child: Text(data.initial, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold))),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -127,14 +127,12 @@ class _NoCall extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
PyrIconBtn(
|
||||
icon: const Icon(Icons.menu_rounded),
|
||||
tooltip: 'Toggle sidebar',
|
||||
onPressed: () => ref.read(railCollapsedProvider.notifier).update((s) => !s),
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
tooltip: 'Zurück',
|
||||
onPressed: () => ref.read(viewModeProvider.notifier).state = ViewMode.chat,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.headphones_rounded, size: 16, color: pt.fgMuted),
|
||||
const SizedBox(width: 8),
|
||||
Text('No active call', style: TextStyle(color: pt.fg, fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
Text('Kein Anruf', style: TextStyle(color: pt.fg, fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -143,9 +141,15 @@ class _NoCall extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('𐃏', style: TextStyle(fontSize: 48, color: pt.fgDim.withAlpha(100))),
|
||||
Icon(Icons.call_end_rounded, size: 48, color: pt.fgDim),
|
||||
const SizedBox(height: 12),
|
||||
Text('Join a voice room to start a call', style: TextStyle(color: pt.fgMuted, fontSize: 14)),
|
||||
Text('Gerade läuft kein Anruf.', style: TextStyle(color: pt.fgMuted, fontSize: 14)),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: () => ref.read(viewModeProvider.notifier).state = ViewMode.chat,
|
||||
style: FilledButton.styleFrom(backgroundColor: pt.accent, foregroundColor: pt.accentFg),
|
||||
child: const Text('Zurück zum Chat'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -193,7 +193,7 @@ class _CallHeader extends StatelessWidget {
|
||||
const Spacer(),
|
||||
PyrIconBtn(
|
||||
icon: Icon(isGridView ? Icons.grid_view_rounded : Icons.featured_video_rounded, color: isGridView ? pt.accent : pt.fg),
|
||||
tooltip: isGridView ? 'Hero mode' : 'Grid mode',
|
||||
tooltip: isGridView ? 'Großansicht' : 'Kachelansicht',
|
||||
onPressed: onToggleView,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
|
||||
@@ -12,6 +12,13 @@ class _ParticipantData {
|
||||
final Color avatarColor;
|
||||
final dynamic originalParticipant;
|
||||
final double aspectRatio;
|
||||
/// Woraus der Anfangsbuchstabe im Avatar gebildet wird (Standard: [name]).
|
||||
final String? initialFrom;
|
||||
|
||||
String get initial {
|
||||
final src = (initialFrom ?? name).replaceFirst('@', '').trim();
|
||||
return src.isEmpty ? '?' : src.characters.first.toUpperCase();
|
||||
}
|
||||
|
||||
const _ParticipantData({
|
||||
required this.id,
|
||||
@@ -24,6 +31,7 @@ class _ParticipantData {
|
||||
this.avatarColor = Colors.grey,
|
||||
this.originalParticipant,
|
||||
this.aspectRatio = 16.0 / 9.0,
|
||||
this.initialFrom,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -148,7 +156,7 @@ class VoiceChannelView extends ConsumerWidget {
|
||||
final hasRemoteVideo = voip.remoteRenderer.srcObject != null && voip.remoteRenderer.srcObject!.getVideoTracks().isNotEmpty;
|
||||
participants.add(_ParticipantData(
|
||||
id: 'remote',
|
||||
name: voip.currentCall?.room.getLocalizedDisplayname() ?? 'Remote',
|
||||
name: voip.currentCall?.room.getLocalizedDisplayname() ?? 'Gegenseite',
|
||||
videoWidget: hasRemoteVideo ? rtc.RTCVideoView(voip.remoteRenderer, objectFit: rtc.RTCVideoViewObjectFit.RTCVideoViewObjectFitContain) : null,
|
||||
isMuted: false,
|
||||
isSpeaking: voip.isRemoteSpeaking,
|
||||
@@ -164,6 +172,7 @@ class VoiceChannelView extends ConsumerWidget {
|
||||
participants.add(_ParticipantData(
|
||||
id: 'local',
|
||||
name: 'Du',
|
||||
initialFrom: voip.currentCall?.room.client.userID?.split(':').first,
|
||||
videoWidget: hasLocalVideo ? rtc.RTCVideoView(voip.localRenderer, objectFit: rtc.RTCVideoViewObjectFit.RTCVideoViewObjectFitContain) : null,
|
||||
isMuted: voip.isMicMuted,
|
||||
isSpeaking: voip.isLocalSpeaking,
|
||||
|
||||
@@ -111,13 +111,13 @@ class _ChatHeader extends ConsumerWidget {
|
||||
PyrIconBtn(
|
||||
size: 28,
|
||||
icon: const Icon(Icons.phone_rounded),
|
||||
tooltip: 'Voice call',
|
||||
tooltip: 'Anrufen',
|
||||
onPressed: () => _startCall(ref, room, cam: false),
|
||||
),
|
||||
PyrIconBtn(
|
||||
size: 28,
|
||||
icon: const Icon(Icons.videocam_rounded),
|
||||
tooltip: 'Video call',
|
||||
tooltip: 'Videoanruf',
|
||||
onPressed: () => _startCall(ref, room, cam: true),
|
||||
),
|
||||
if (size.width > 700)
|
||||
@@ -129,12 +129,6 @@ class _ChatHeader extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
if (size.width > 800) ...[
|
||||
PyrIconBtn(
|
||||
size: 28,
|
||||
icon: const Icon(Icons.inbox_rounded),
|
||||
tooltip: 'Threads',
|
||||
onPressed: () {},
|
||||
),
|
||||
PyrIconBtn(
|
||||
size: 28,
|
||||
icon: const Icon(Icons.push_pin_rounded),
|
||||
@@ -557,13 +551,13 @@ class _DmExpandingHeaderState extends ConsumerState<_DmExpandingHeader> {
|
||||
PyrIconBtn(
|
||||
size: 28,
|
||||
icon: const Icon(Icons.phone_rounded),
|
||||
tooltip: 'Voice call',
|
||||
tooltip: 'Anrufen',
|
||||
onPressed: () => _startCall(cam: false),
|
||||
),
|
||||
PyrIconBtn(
|
||||
size: 28,
|
||||
icon: const Icon(Icons.videocam_rounded),
|
||||
tooltip: 'Video call',
|
||||
tooltip: 'Videoanruf',
|
||||
onPressed: () => _startCall(cam: true),
|
||||
),
|
||||
if (size.width > 700)
|
||||
|
||||
@@ -65,7 +65,7 @@ class _MessageList extends StatelessWidget {
|
||||
Icon(Icons.forum_outlined, size: 48, color: pt.fgDim),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No messages yet',
|
||||
'Noch keine Nachrichten – schreib die erste!',
|
||||
style: TextStyle(color: pt.fgMuted, fontSize: 14),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -120,7 +120,7 @@ class NoChatSelected extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Choose from the sidebar on the left',
|
||||
'Links findest du alle Chats und Räume.',
|
||||
style: TextStyle(color: pt.fgDim, fontSize: 13),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -535,7 +535,7 @@ class _ChatViewState extends ConsumerState<ChatView> {
|
||||
),
|
||||
error: (e, _) => Center(
|
||||
child: Text(
|
||||
'Could not load messages',
|
||||
'Nachrichten konnten nicht geladen werden.',
|
||||
style: TextStyle(color: pt.fgDim, fontSize: 14),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:pyramid/core/matrix_client.dart';
|
||||
import 'package:pyramid/core/theme.dart';
|
||||
import 'package:pyramid/widgets/mxc_image.dart';
|
||||
import 'package:pyramid/widgets/pyramid_logo.dart';
|
||||
|
||||
class MembersPanel extends ConsumerWidget {
|
||||
@@ -94,7 +95,7 @@ class MembersPanel extends ConsumerWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 24),
|
||||
child: Text(
|
||||
'No members loaded',
|
||||
'Keine Mitglieder geladen',
|
||||
style: TextStyle(color: pt.fgDim, fontSize: 12),
|
||||
),
|
||||
),
|
||||
@@ -177,19 +178,22 @@ class _MemberItemState extends State<_MemberItem> {
|
||||
opacity: widget.isOffline ? 0.5 : 1,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(pt.rSm),
|
||||
),
|
||||
child: Center(
|
||||
// Rund wie überall sonst, mit echtem Profilbild.
|
||||
MxcAvatar(
|
||||
mxcUri: member.avatarUrl,
|
||||
client: member.room.client,
|
||||
size: 32,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
placeholder: (_) => Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
initial,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@@ -214,7 +218,7 @@ class _MemberItemState extends State<_MemberItem> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
member.id == member.room.client.userID ? '$name (Du)' : name,
|
||||
style: TextStyle(
|
||||
color: widget.isOffline ? pt.fgMuted : pt.fg,
|
||||
fontSize: 13,
|
||||
|
||||
@@ -112,7 +112,7 @@ class _RoomsPanelState extends ConsumerState<RoomsPanel> {
|
||||
),
|
||||
),
|
||||
error: (e, _) => Center(
|
||||
child: Text('Error',
|
||||
child: Text('Fehler',
|
||||
style: TextStyle(color: pt.fgDim, fontSize: 12)),
|
||||
),
|
||||
data: (rooms) => _RoomsList(
|
||||
@@ -136,7 +136,11 @@ class _RoomsPanelState extends ConsumerState<RoomsPanel> {
|
||||
final hasVoipCall = voip.currentCall != null &&
|
||||
voip.currentCall!.state != CallState.kEnded &&
|
||||
voip.currentCall!.state != CallState.kRinging;
|
||||
final hasCallDock = call.isActive || hasVoipCall;
|
||||
// Am PC bei offener Anrufansicht nicht doppelt steuern
|
||||
// (UI-Audit S6); am Handy liegt die Anrufansicht ohnehin darüber.
|
||||
final callViewOpen = ref.watch(viewModeProvider) == ViewMode.voice &&
|
||||
!isMobileLayout(MediaQuery.sizeOf(context));
|
||||
final hasCallDock = !callViewOpen && (call.isActive || hasVoipCall);
|
||||
final isLandscapeCompact =
|
||||
MediaQuery.sizeOf(context).height < 500;
|
||||
|
||||
@@ -147,8 +151,8 @@ class _RoomsPanelState extends ConsumerState<RoomsPanel> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (call.isActive) MiniCallWidget(call: call),
|
||||
if (hasVoipCall) MiniCallWidget(call: voip),
|
||||
if (call.isActive && !callViewOpen) MiniCallWidget(call: call),
|
||||
if (hasVoipCall && !callViewOpen) MiniCallWidget(call: voip),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:pyramid/core/app_state.dart';
|
||||
import 'package:pyramid/core/appearance.dart' show isMobileLayout;
|
||||
import 'package:pyramid/core/matrix_client.dart';
|
||||
import 'package:pyramid/core/room_kinds.dart';
|
||||
import 'package:pyramid/core/theme.dart';
|
||||
import 'package:pyramid/features/rooms/rooms_provider.dart';
|
||||
import 'package:pyramid/widgets/create_join_dialog.dart';
|
||||
@@ -32,8 +34,25 @@ class SpacesRail extends ConsumerWidget {
|
||||
}
|
||||
// Switch space and restore last visited room for the new space
|
||||
ref.read(activeSpaceIdProvider.notifier).state = id;
|
||||
ref.read(activeRoomIdProvider.notifier).state =
|
||||
ref.read(lastRoomPerSpaceProvider)[id];
|
||||
var target = ref.read(lastRoomPerSpaceProvider)[id];
|
||||
// Am PC nicht auf „Wähle einen Chat aus“ landen: ersten Textkanal des
|
||||
// Spaces öffnen (UI-Audit S5). Am Handy bleibt die Kanalliste sichtbar.
|
||||
if (target == null &&
|
||||
id != null &&
|
||||
id != 'dms' &&
|
||||
id != 'rooms' &&
|
||||
!isMobileLayout(MediaQuery.sizeOf(context))) {
|
||||
final client = ref.read(matrixClientProvider).valueOrNull;
|
||||
final space = client?.getRoomById(id);
|
||||
for (final child in space?.spaceChildren ?? []) {
|
||||
final r = child.roomId == null ? null : client!.getRoomById(child.roomId!);
|
||||
if (r != null && r.membership == Membership.join && !isVoiceChannelRoom(r)) {
|
||||
target = r.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ref.read(activeRoomIdProvider.notifier).state = target;
|
||||
final scaffold = Scaffold.maybeOf(context);
|
||||
if (scaffold != null && scaffold.isDrawerOpen) {
|
||||
scaffold.closeDrawer();
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'package:pyramid/features/rooms/rooms_panel.dart';
|
||||
import 'package:pyramid/features/spaces/spaces_rail.dart';
|
||||
import 'package:pyramid/layout/mobile_page_stack.dart';
|
||||
import 'package:pyramid/widgets/spring_press.dart' show motionDuration;
|
||||
import 'package:pyramid/widgets/mxc_image.dart';
|
||||
import 'package:pyramid/widgets/search_modal.dart';
|
||||
import 'package:pyramid/widgets/settings_modal.dart';
|
||||
import 'package:pyramid/widgets/share_target_dialog.dart';
|
||||
@@ -151,6 +152,44 @@ class _AppShellState extends ConsumerState<AppShell>
|
||||
ref.read(activeModalProvider.notifier).state = ModalKind.none;
|
||||
}
|
||||
|
||||
bool _hadVoipCall = false;
|
||||
bool _hadLiveKit = false;
|
||||
|
||||
/// Anruf ist vorbei: Anrufansicht verlassen (sofern kein anderer Anruf
|
||||
/// mehr läuft) und kurz Bescheid geben.
|
||||
void _leaveCallView(String message) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
try {
|
||||
_leaveCallViewNow(message);
|
||||
} catch (e, s) {
|
||||
debugPrint('[AppShell] Anrufansicht verlassen: $e $s');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _leaveCallViewNow(String message) {
|
||||
{
|
||||
if (!mounted) return;
|
||||
final voip = ref.read(callSignalingProvider);
|
||||
final lk = ref.read(voiceChannelProvider);
|
||||
final stillInCall = (voip.currentCall != null &&
|
||||
voip.currentCall!.state != CallState.kEnded) ||
|
||||
lk.isActive ||
|
||||
lk.isConnecting;
|
||||
if (stillInCall) return;
|
||||
if (ref.read(viewModeProvider) == ViewMode.voice) {
|
||||
ref.read(viewModeProvider.notifier).state = ViewMode.chat;
|
||||
}
|
||||
ScaffoldMessenger.maybeOf(context)
|
||||
?..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(
|
||||
content: Text(message),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 3),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pt = PyramidTheme.of(context);
|
||||
@@ -164,6 +203,12 @@ class _AppShellState extends ConsumerState<AppShell>
|
||||
// würde der Nutzer permanent zurück in die Call-UI gezwungen und könnte
|
||||
// sie nie minimieren.
|
||||
ref.listen(callSignalingProvider, (prev, next) {
|
||||
// Anruf vorbei (aufgelegt, abgelehnt, gescheitert) → nicht auf der
|
||||
// leeren Anrufansicht stehen lassen (UI-Audit S1, 2026-10-08).
|
||||
final hasCall = next.currentCall != null &&
|
||||
next.currentCall!.state != CallState.kEnded;
|
||||
if (_hadVoipCall && !hasCall) _leaveCallView('Anruf beendet');
|
||||
_hadVoipCall = hasCall;
|
||||
final wasConnected = _wasVoipConnected;
|
||||
final isConnected = next.currentCall != null &&
|
||||
next.currentCall!.state == CallState.kConnected;
|
||||
@@ -176,6 +221,12 @@ class _AppShellState extends ConsumerState<AppShell>
|
||||
});
|
||||
|
||||
ref.listen(voiceChannelProvider, (prev, next) {
|
||||
final lkActive = next.isActive || next.isConnecting;
|
||||
if (_hadLiveKit && !lkActive) {
|
||||
// Fehler (z. B. Verbindung verloren) sichtbar machen statt still.
|
||||
_leaveCallView(next.error ?? (next.isVoiceChannel ? 'Sprachkanal verlassen' : 'Anruf beendet'));
|
||||
}
|
||||
_hadLiveKit = lkActive;
|
||||
// Only auto-switch to voice view for video/audio calls, NOT voice channels
|
||||
// (voice channels should stay in background with a mini bar)
|
||||
final wasActive = _wasLiveKitActive;
|
||||
@@ -519,18 +570,27 @@ class _IncomingCallOverlay extends ConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: pt.accent,
|
||||
shape: BoxShape.circle,
|
||||
MxcAvatar(
|
||||
mxcUri: call.room.avatar,
|
||||
client: call.room.client,
|
||||
size: 88,
|
||||
placeholder: (_) => Container(
|
||||
width: 88,
|
||||
height: 88,
|
||||
decoration: BoxDecoration(
|
||||
color: pt.accent,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
call.room.getLocalizedDisplayname().characters.firstOrNull?.toUpperCase() ?? '?',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 36, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
child: const Icon(Icons.person_rounded, size: 40, color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Eingehender Anruf',
|
||||
call.type == CallType.kVideo ? 'Eingehender Videoanruf' : 'Eingehender Anruf',
|
||||
style: TextStyle(color: pt.fgDim, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
@@ -214,7 +214,7 @@ class _SettingsModalState extends ConsumerState<SettingsModal> {
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
children: [
|
||||
for (final c in _categories)
|
||||
for (final c in _categories.where((c) => c.$1 != 'keys'))
|
||||
_MobileCategoryTile(
|
||||
label: c.$2,
|
||||
icon: c.$3,
|
||||
|
||||
Reference in New Issue
Block a user