Files
pyramid/lib/widgets/screen_share_picker.dart
T
Bernd SteckmeisterandClaude Opus 5.5 f46cc22aae fix(calls): Taub wirkt, Stumm-Knopf zeigt echten Zustand, Mini-Leiste erst nach Annehmen, HDR-Hinweis
Bernds Test 2026-10-08:
- Taub-Knopf setzte bei 1:1-Anrufen nur ein Häkchen. Jetzt wie Discord:
  Ton der Gegenseite aus (Empfangsspuren deaktiviert, auch nach
  Neuverhandlung) und selbst stumm; beim Aufheben Mikro-Zustand von vorher.
- Stumm: schlug nur das Senden der Status-Info fehl, schaltete Pyramid den
  Knopf blind zurück, obwohl das Mikro aus war – danach zeigte der Knopf
  das Gegenteil. Jetzt wird der echte SDK-Zustand übernommen.
- Mini-Anrufleiste erschien schon beim Klingeln (vor dem Annehmen).
- Bildschirm-Auswähler: deutsch, Hinweis zur bekannten WebRTC-Grenze bei
  HDR unter Windows (blasses Bild; Fenster teilen oder Win+Alt+B).
- Live-Anruftest drückt Stumm und Taub über die echten Knöpfe und prüft,
  was bei der Gegenseite ankommt.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 16:20:48 +02:00

256 lines
8.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:pyramid/core/theme.dart';
class ScreenSharePicker extends StatefulWidget {
const ScreenSharePicker({super.key});
static Future<DesktopCapturerSource?> show(BuildContext context) async {
return showDialog<DesktopCapturerSource>(
context: context,
builder: (context) => const ScreenSharePicker(),
);
}
@override
State<ScreenSharePicker> createState() => _ScreenSharePickerState();
}
class _ScreenSharePickerState extends State<ScreenSharePicker> with SingleTickerProviderStateMixin {
late TabController _tabController;
List<DesktopCapturerSource> _sources = [];
bool _loading = true;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_loadSources();
}
Future<void> _loadSources() async {
try {
// FluffyChat Fix: Get both types at once to avoid native crashes on Windows
final sources = await desktopCapturer.getSources(types: [
SourceType.Screen,
SourceType.Window,
]);
if (mounted) {
setState(() {
_sources = sources;
_loading = false;
});
}
} catch (e) {
debugPrint('[Picker] Error loading sources: $e');
if (mounted) setState(() => _loading = false);
}
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final pt = PyramidTheme.of(context);
final screens = _sources.where((s) => s.type == SourceType.Screen).toList();
final windows = _sources.where((s) => s.type == SourceType.Window).toList();
final screenSize = MediaQuery.sizeOf(context);
return Center(
child: Container(
// Auf kleinen Bildschirmen (Mobile, Quer- wie Hochformat) einpassen.
width: (screenSize.width - 32).clamp(280.0, 600.0),
height: (screenSize.height - 48).clamp(320.0, 520.0),
decoration: BoxDecoration(
color: pt.bg1,
borderRadius: BorderRadius.circular(pt.rXl),
border: Border.all(color: pt.border),
boxShadow: [BoxShadow(color: Colors.black54, blurRadius: 40)],
),
child: Column(
children: [
// Header
Padding(
padding: const EdgeInsets.fromLTRB(24, 20, 16, 8),
child: Row(
children: [
Text('Bildschirm teilen',
style: TextStyle(color: pt.fg, fontSize: 20, fontWeight: FontWeight.w700)),
const Spacer(),
IconButton(
icon: Icon(Icons.close_rounded, color: pt.fgDim),
onPressed: () => Navigator.pop(context),
),
],
),
),
// Tabs
TabBar(
controller: _tabController,
indicatorColor: pt.accent,
labelColor: pt.accent,
unselectedLabelColor: pt.fgMuted,
dividerColor: pt.border,
tabs: const [
Tab(text: 'Bildschirme'),
Tab(text: 'Fenster'),
],
),
// Content
Expanded(
child: _loading
? Center(child: CircularProgressIndicator(color: pt.accent))
: TabBarView(
controller: _tabController,
children: [
_SourceGrid(sources: screens, pt: pt),
_SourceGrid(sources: windows, pt: pt),
],
),
),
// Footer
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: pt.bg2,
borderRadius: BorderRadius.vertical(bottom: Radius.circular(pt.rXl)),
border: Border(top: BorderSide(color: pt.border)),
),
child: Row(
children: [
// Bekannte WebRTC-Grenze unter Windows: Bei eingeschaltetem
// HDR liefert die Bildschirmaufnahme (DXGI) ein blasses,
// zu helles Bild – Zoom/Discord haben dasselbe. Abhilfe:
// Fenster teilen oder HDR kurz umschalten (Bernd 2026-10-08).
if (Theme.of(context).platform == TargetPlatform.windows)
Expanded(
child: Row(
children: [
Icon(Icons.hdr_on_rounded, size: 18, color: pt.fgDim),
const SizedBox(width: 8),
Expanded(
child: Text(
'HDR an? Dann wirkt ein geteilter Bildschirm blass. '
'Besser ein Fenster teilen oder HDR kurz umschalten '
'(Win + Alt + B).',
style: TextStyle(color: pt.fgDim, fontSize: 11.5),
),
),
],
),
)
else
const Spacer(),
const SizedBox(width: 12),
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted)),
),
],
),
),
],
),
),
);
}
}
class _SourceGrid extends StatelessWidget {
final List<DesktopCapturerSource> sources;
final PyramidTheme pt;
const _SourceGrid({required this.sources, required this.pt});
@override
Widget build(BuildContext context) {
if (sources.isEmpty) {
return Center(child: Text('Keine Quellen gefunden', style: TextStyle(color: pt.fgDim)));
}
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 16,
childAspectRatio: 1.4,
),
itemCount: sources.length,
itemBuilder: (context, i) {
final s = sources[i];
return _SourceTile(source: s, pt: pt);
},
);
}
}
class _SourceTile extends StatefulWidget {
final DesktopCapturerSource source;
final PyramidTheme pt;
const _SourceTile({required this.source, required this.pt});
@override
State<_SourceTile> createState() => _SourceTileState();
}
class _SourceTileState extends State<_SourceTile> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final pt = widget.pt;
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => Navigator.pop(context, widget.source),
child: Column(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: pt.bg3,
borderRadius: BorderRadius.circular(pt.rBase),
border: Border.all(
color: _hovered ? pt.accent : pt.border,
width: _hovered ? 2 : 1,
),
),
clipBehavior: Clip.antiAlias,
child: widget.source.thumbnail != null
? Image.memory(
widget.source.thumbnail!,
fit: BoxFit.cover,
width: double.infinity,
)
: Center(child: Icon(Icons.monitor_rounded, color: pt.fgDim, size: 32)),
),
),
const SizedBox(height: 6),
Text(
widget.source.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: _hovered ? pt.fg : pt.fgMuted,
fontSize: 12,
fontWeight: _hovered ? FontWeight.w600 : FontWeight.w400,
),
),
],
),
),
);
}
}