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