Files
pyramid/lib/widgets/share_target_dialog.dart
T
Bernd Steckmeister 99f10c5210 style: doppelte Unterstriche in Closure-Parametern bereinigen
Dart 3 erlaubt mehrere Wildcard-Parameter mit demselben Namen "_" in
einem Scope, daher sind __ / ___ für ungenutzte Callback-Parameter
(pageBuilder, errorBuilder etc.) unnötig. Rein mechanische Änderung,
keine Verhaltensänderung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 06:30:40 +02:00

298 lines
10 KiB
Dart

import 'dart:io';
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/matrix_client.dart';
import 'package:pyramid/core/theme.dart';
import 'package:pyramid/features/chat/attachment_dialog.dart';
import 'package:pyramid/widgets/mxc_image.dart';
/// "Teilen nach Pyramid": Raum-Picker für geteilte Inhalte (Text/Dateien).
/// Nach Auswahl wird der Raum geöffnet; Dateien laufen durch den
/// AttachmentDialog (mit Vorschau), Text wird direkt gesendet.
class ShareTargetDialog {
static Future<void> show(
BuildContext context,
WidgetRef ref,
PendingShare share,
) async {
final client = await ref.read(matrixClientProvider.future);
if (!context.mounted) return;
final room = await showGeneralDialog<Room>(
context: context,
barrierDismissible: true,
barrierLabel: 'Teilen',
barrierColor: Colors.black54,
transitionDuration: const Duration(milliseconds: 180),
pageBuilder: (_, _, _) => _ShareTargetPicker(client: client, share: share),
transitionBuilder: (_, anim, _, child) {
final curved = CurvedAnimation(parent: anim, curve: Curves.easeOutBack);
return FadeTransition(
opacity: anim,
child: ScaleTransition(
scale: Tween(begin: 0.94, end: 1.0).animate(curved),
child: child,
),
);
},
);
if (room == null || !context.mounted) return;
// Zielraum öffnen.
ref.read(activeRoomIdProvider.notifier).state = room.id;
ref.read(viewModeProvider.notifier).state = ViewMode.chat;
var filesSent = false;
if (share.paths.isNotEmpty) {
final files = share.paths
.map(File.new)
.where((f) => f.existsSync())
.toList();
if (files.isNotEmpty) {
filesSent = await AttachmentDialog.show(context, files, room);
for (final f in files) {
f.delete().catchError((_) => f);
}
}
}
final text = share.text?.trim() ?? '';
if (text.isNotEmpty && (share.paths.isEmpty || filesSent)) {
try {
await room.sendTextEvent(text);
} catch (_) {}
}
}
}
class _ShareTargetPicker extends StatefulWidget {
final Client client;
final PendingShare share;
const _ShareTargetPicker({required this.client, required this.share});
@override
State<_ShareTargetPicker> createState() => _ShareTargetPickerState();
}
class _ShareTargetPickerState extends State<_ShareTargetPicker> {
String _filter = '';
List<Room> get _rooms {
final rooms = widget.client.rooms
.where((r) =>
r.membership == Membership.join &&
!r.isSpace &&
(_filter.isEmpty ||
r
.getLocalizedDisplayname()
.toLowerCase()
.contains(_filter.toLowerCase())))
.toList()
// Zuletzt aktive Chats zuerst — wie bei Discord/WhatsApp-Share.
..sort((a, b) => (b.lastEvent?.originServerTs ??
DateTime.fromMillisecondsSinceEpoch(0))
.compareTo(a.lastEvent?.originServerTs ??
DateTime.fromMillisecondsSinceEpoch(0)));
return rooms;
}
@override
Widget build(BuildContext context) {
final pt = PyramidTheme.of(context);
final size = MediaQuery.sizeOf(context);
final rooms = _rooms;
final share = widget.share;
final fileCount = share.paths.length;
return Center(
child: Material(
color: Colors.transparent,
child: Container(
width: (size.width - 32).clamp(280.0, 440.0),
height: (size.height - 80).clamp(320.0, 560.0),
decoration: BoxDecoration(
color: pt.bg1,
borderRadius: BorderRadius.circular(pt.rXl),
border: Border.all(color: pt.border),
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 40)],
),
child: Column(
children: [
// Header
Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 12, 4),
child: Row(
children: [
Icon(Icons.share_rounded, size: 18, color: pt.accent),
const SizedBox(width: 10),
Expanded(
child: Text(
'Teilen nach…',
style: TextStyle(
color: pt.fg,
fontSize: 16,
fontWeight: FontWeight.w700),
),
),
IconButton(
icon: Icon(Icons.close_rounded, size: 18, color: pt.fgDim),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
// Vorschau des geteilten Inhalts
Padding(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 10),
child: Container(
width: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: pt.bg2,
borderRadius: BorderRadius.circular(pt.rBase),
border: Border.all(color: pt.border),
),
child: Row(
children: [
Icon(
fileCount > 0
? Icons.attach_file_rounded
: Icons.notes_rounded,
size: 14,
color: pt.fgMuted,
),
const SizedBox(width: 8),
Expanded(
child: Text(
fileCount > 0
? '$fileCount Datei${fileCount == 1 ? '' : 'en'}'
'${(share.text?.isNotEmpty ?? false) ? ' + Text' : ''}'
: (share.text ?? ''),
style: TextStyle(color: pt.fgMuted, fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
// Suche
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 8),
child: TextField(
autofocus: false,
onChanged: (v) => setState(() => _filter = v),
style: TextStyle(color: pt.fg, fontSize: 13),
decoration: InputDecoration(
hintText: 'Chat suchen…',
hintStyle: TextStyle(color: pt.fgDim, fontSize: 13),
prefixIcon:
Icon(Icons.search_rounded, size: 16, color: pt.fgDim),
isDense: true,
filled: true,
fillColor: pt.bg2,
contentPadding: const EdgeInsets.symmetric(vertical: 8),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(pt.rBase),
borderSide: BorderSide(color: pt.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(pt.rBase),
borderSide: BorderSide(color: pt.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(pt.rBase),
borderSide: BorderSide(color: pt.accent),
),
),
),
),
// Raumliste
Expanded(
child: rooms.isEmpty
? Center(
child: Text('Keine Chats gefunden',
style: TextStyle(color: pt.fgDim, fontSize: 13)),
)
: ListView.builder(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 8),
itemCount: rooms.length,
itemBuilder: (_, i) =>
_RoomRow(room: rooms[i], pt: pt, client: widget.client),
),
),
],
),
),
),
);
}
}
class _RoomRow extends StatelessWidget {
final Room room;
final PyramidTheme pt;
final Client client;
const _RoomRow({required this.room, required this.pt, required this.client});
@override
Widget build(BuildContext context) {
final name = room.getLocalizedDisplayname();
final initial = name.isNotEmpty ? name[0].toUpperCase() : '?';
return InkWell(
borderRadius: BorderRadius.circular(pt.rBase),
onTap: () => Navigator.of(context).pop(room),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
if (room.avatar != null)
MxcAvatar(
mxcUri: room.avatar,
client: client,
size: 30,
borderRadius: BorderRadius.circular(15),
placeholder: (_) => _initialCircle(initial),
)
else
_initialCircle(initial),
const SizedBox(width: 12),
Expanded(
child: Text(
name,
style: TextStyle(
color: pt.fg, fontSize: 13, fontWeight: FontWeight.w500),
overflow: TextOverflow.ellipsis,
),
),
if (room.isDirectChat)
Icon(Icons.person_outline_rounded, size: 14, color: pt.fgDim)
else
Icon(Icons.tag_rounded, size: 14, color: pt.fgDim),
],
),
),
);
}
Widget _initialCircle(String initial) => Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: pt.accent.withAlpha(60),
shape: BoxShape.circle,
),
child: Center(
child: Text(
initial,
style: TextStyle(
color: pt.fg, fontSize: 13, fontWeight: FontWeight.w600),
),
),
);
}