wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)
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
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user