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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user