99f10c5210
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>
298 lines
10 KiB
Dart
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),
|
|
),
|
|
),
|
|
);
|
|
}
|