25ed765a03
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.) als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
234 lines
7.1 KiB
Dart
234 lines
7.1 KiB
Dart
import 'dart:typed_data';
|
|
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) {
|
|
print('[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('Share your screen',
|
|
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: 'Screens'),
|
|
Tab(text: 'Windows'),
|
|
],
|
|
),
|
|
|
|
// 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(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text('Cancel', 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('No sources found', 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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|