refactor: Gott-Datei space_admin_dialog.dart in 6 Dateien aufgeteilt
Gleiche Technik wie bei den vorigen drei Umbauten: reine Verschiebung per Dart part/part-of, keine Logikänderung. space_admin_dialog.dart schrumpft von 2278 auf 19 Zeilen (nur noch Bibliothekskopf). Verifiziert per automatisiertem Blockvergleich gegen das Original und flutter analyze. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,587 @@
|
||||
part of '../space_admin_dialog.dart';
|
||||
|
||||
const _kSpaceChild = 'm.space.child';
|
||||
const _kSpaceParent = 'm.space.parent';
|
||||
const _kVoiceChannelType = 'io.pyramid.voice_channel';
|
||||
|
||||
class _ChannelsTab extends ConsumerStatefulWidget {
|
||||
final Room space;
|
||||
const _ChannelsTab({required this.space});
|
||||
|
||||
@override
|
||||
ConsumerState<_ChannelsTab> createState() => _ChannelsTabState();
|
||||
}
|
||||
|
||||
class _ChannelsTabState extends ConsumerState<_ChannelsTab> {
|
||||
bool _busy = false;
|
||||
|
||||
List<({String roomId, Room? room, bool isVoice})> _getChildren(Client client) {
|
||||
final childStates = widget.space.states[_kSpaceChild] ?? {};
|
||||
final result = <({String roomId, Room? room, bool isVoice})>[];
|
||||
for (final entry in childStates.entries) {
|
||||
final content = entry.value.content;
|
||||
if (content.isEmpty) continue;
|
||||
final childRoomId = entry.key;
|
||||
final room = client.getRoomById(childRoomId);
|
||||
final createContent = room?.getState('m.room.create')?.content;
|
||||
final isVoice = createContent?['type'] == _kVoiceChannelType;
|
||||
result.add((roomId: childRoomId, room: room, isVoice: isVoice));
|
||||
}
|
||||
result.sort((a, b) {
|
||||
final nameA = a.room?.getLocalizedDisplayname() ?? a.roomId;
|
||||
final nameB = b.room?.getLocalizedDisplayname() ?? b.roomId;
|
||||
return nameA.compareTo(nameB);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _removeChild(String roomId) async {
|
||||
final client = ref.read(matrixClientProvider).valueOrNull;
|
||||
if (client == null) return;
|
||||
setState(() => _busy = true);
|
||||
try {
|
||||
await client.setRoomStateWithKey(widget.space.id, _kSpaceChild, roomId, {});
|
||||
if (mounted) setState(() {});
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addChild(String roomId) async {
|
||||
final client = ref.read(matrixClientProvider).valueOrNull;
|
||||
if (client == null) return;
|
||||
final serverName = client.userID?.split(':').last ?? '';
|
||||
try {
|
||||
await client.setRoomStateWithKey(
|
||||
widget.space.id,
|
||||
_kSpaceChild,
|
||||
roomId,
|
||||
{'via': [serverName], 'suggested': false},
|
||||
);
|
||||
// Best-effort: set parent in child room
|
||||
try {
|
||||
await client.setRoomStateWithKey(
|
||||
roomId,
|
||||
_kSpaceParent,
|
||||
widget.space.id,
|
||||
{'via': [serverName], 'canonical': true},
|
||||
);
|
||||
} catch (_) {}
|
||||
if (mounted) setState(() {});
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createAndAdd({
|
||||
required String name,
|
||||
required bool isVoice,
|
||||
required Client client,
|
||||
}) async {
|
||||
setState(() => _busy = true);
|
||||
try {
|
||||
final serverName = client.userID?.split(':').last ?? '';
|
||||
final roomId = await client.createRoom(
|
||||
name: name,
|
||||
visibility: Visibility.private,
|
||||
preset: CreateRoomPreset.privateChat,
|
||||
creationContent: isVoice ? {'type': _kVoiceChannelType} : null,
|
||||
initialState: [
|
||||
StateEvent(
|
||||
type: 'm.room.join_rules',
|
||||
content: {'join_rule': 'restricted', 'allow': [
|
||||
{'type': 'm.room_membership', 'room_id': widget.space.id}
|
||||
]},
|
||||
),
|
||||
],
|
||||
);
|
||||
await client.setRoomStateWithKey(
|
||||
widget.space.id,
|
||||
_kSpaceChild,
|
||||
roomId,
|
||||
{'via': [serverName], 'suggested': false},
|
||||
);
|
||||
try {
|
||||
await client.setRoomStateWithKey(
|
||||
roomId,
|
||||
_kSpaceParent,
|
||||
widget.space.id,
|
||||
{'via': [serverName], 'canonical': true},
|
||||
);
|
||||
} catch (_) {}
|
||||
if (mounted) setState(() {});
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _showCreateDialog(BuildContext context, Client client) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => _CreateChannelDialog(
|
||||
onConfirm: (name, isVoice) =>
|
||||
_createAndAdd(name: name, isVoice: isVoice, client: client),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddExistingDialog(BuildContext context, Client client) {
|
||||
final childStates = widget.space.states[_kSpaceChild] ?? {};
|
||||
final existingIds = {
|
||||
for (final e in childStates.entries)
|
||||
if (e.value.content.isNotEmpty) e.key
|
||||
};
|
||||
final available = client.rooms
|
||||
.where((r) => !r.isSpace && !existingIds.contains(r.id))
|
||||
.toList()
|
||||
..sort((a, b) => a.getLocalizedDisplayname()
|
||||
.compareTo(b.getLocalizedDisplayname()));
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => _AddExistingDialog(
|
||||
rooms: available,
|
||||
client: client,
|
||||
onConfirm: (roomId) => _addChild(roomId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pt = PyramidTheme.of(context);
|
||||
final client = ref.watch(matrixClientProvider).valueOrNull;
|
||||
final ownPower = widget.space.ownPowerLevel;
|
||||
final canManage = ownPower >= _kPowerMod;
|
||||
|
||||
if (client == null) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(color: pt.accent, strokeWidth: 2));
|
||||
}
|
||||
|
||||
final children = _getChildren(client);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (canManage)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Kanal erstellen'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: pt.accent,
|
||||
side: BorderSide(color: pt.accent),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rSm)),
|
||||
),
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () => _showCreateDialog(context, client),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.link, size: 16),
|
||||
label: const Text('Raum hinzufügen'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: pt.fgDim,
|
||||
side: BorderSide(color: pt.border),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rSm)),
|
||||
),
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () => _showAddExistingDialog(context, client),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (children.isEmpty)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.forum_outlined, color: pt.fgDim, size: 40),
|
||||
const SizedBox(height: 8),
|
||||
Text('Noch keine Kanäle',
|
||||
style: TextStyle(color: pt.fgDim, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
itemCount: children.length,
|
||||
itemBuilder: (context, i) {
|
||||
final child = children[i];
|
||||
final name = child.room?.getLocalizedDisplayname() ??
|
||||
child.roomId;
|
||||
return _ChannelItem(
|
||||
name: name,
|
||||
isVoice: child.isVoice,
|
||||
roomId: child.roomId,
|
||||
pt: pt,
|
||||
canRemove: canManage,
|
||||
onRemove: () => _removeChild(child.roomId),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChannelItem extends StatefulWidget {
|
||||
final String name;
|
||||
final bool isVoice;
|
||||
final String roomId;
|
||||
final PyramidTheme pt;
|
||||
final bool canRemove;
|
||||
final VoidCallback onRemove;
|
||||
|
||||
const _ChannelItem({
|
||||
required this.name,
|
||||
required this.isVoice,
|
||||
required this.roomId,
|
||||
required this.pt,
|
||||
required this.canRemove,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ChannelItem> createState() => _ChannelItemState();
|
||||
}
|
||||
|
||||
class _ChannelItemState extends State<_ChannelItem> {
|
||||
bool _hovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pt = widget.pt;
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: _hovered ? pt.bg2 : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(pt.rSm),
|
||||
),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
|
||||
leading: Icon(
|
||||
widget.isVoice
|
||||
? Icons.volume_up_outlined
|
||||
: Icons.tag,
|
||||
color: pt.fgDim,
|
||||
size: 18,
|
||||
),
|
||||
title: Text(
|
||||
widget.name,
|
||||
style: TextStyle(
|
||||
color: pt.fg, fontSize: 13, fontWeight: FontWeight.w500),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
widget.isVoice ? 'Sprachkanal' : 'Textkanal',
|
||||
style: TextStyle(color: pt.fgDim, fontSize: 11),
|
||||
),
|
||||
trailing: widget.canRemove && _hovered
|
||||
? IconButton(
|
||||
icon: Icon(Icons.link_off,
|
||||
color: const Color(0xFFED4245), size: 18),
|
||||
tooltip: 'Aus Space entfernen',
|
||||
onPressed: widget.onRemove,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateChannelDialog extends StatefulWidget {
|
||||
final void Function(String name, bool isVoice) onConfirm;
|
||||
const _CreateChannelDialog({required this.onConfirm});
|
||||
|
||||
@override
|
||||
State<_CreateChannelDialog> createState() => _CreateChannelDialogState();
|
||||
}
|
||||
|
||||
class _CreateChannelDialogState extends State<_CreateChannelDialog> {
|
||||
final _ctrl = TextEditingController();
|
||||
bool _isVoice = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pt = PyramidTheme.of(context);
|
||||
return AlertDialog(
|
||||
backgroundColor: pt.bg1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rLg)),
|
||||
title: Text('Neuen Kanal erstellen',
|
||||
style: TextStyle(color: pt.fg, fontWeight: FontWeight.w700)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_SectionLabel('Name', pt),
|
||||
const SizedBox(height: 6),
|
||||
_Field(controller: _ctrl, hint: 'kanal-name', pt: pt),
|
||||
const SizedBox(height: 16),
|
||||
_SectionLabel('Typ', pt),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _isVoice = false),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: !_isVoice ? pt.accent.withAlpha(40) : pt.bg0,
|
||||
border: Border.all(
|
||||
color: !_isVoice ? pt.accent : pt.border),
|
||||
borderRadius: BorderRadius.circular(pt.rSm),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.tag,
|
||||
color: !_isVoice ? pt.accent : pt.fgDim,
|
||||
size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Text('Text',
|
||||
style: TextStyle(
|
||||
color: !_isVoice ? pt.accent : pt.fgDim,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _isVoice = true),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: _isVoice ? pt.accent.withAlpha(40) : pt.bg0,
|
||||
border: Border.all(
|
||||
color: _isVoice ? pt.accent : pt.border),
|
||||
borderRadius: BorderRadius.circular(pt.rSm),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.volume_up_outlined,
|
||||
color: _isVoice ? pt.accent : pt.fgDim,
|
||||
size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Text('Sprache',
|
||||
style: TextStyle(
|
||||
color: _isVoice ? pt.accent : pt.fgDim,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Abbrechen')),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(backgroundColor: pt.accent),
|
||||
onPressed: () {
|
||||
final name = _ctrl.text.trim();
|
||||
if (name.isEmpty) return;
|
||||
Navigator.pop(context);
|
||||
widget.onConfirm(name, _isVoice);
|
||||
},
|
||||
child: const Text('Erstellen',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AddExistingDialog extends StatefulWidget {
|
||||
final List<Room> rooms;
|
||||
final Client client;
|
||||
final void Function(String roomId) onConfirm;
|
||||
|
||||
const _AddExistingDialog({
|
||||
required this.rooms,
|
||||
required this.client,
|
||||
required this.onConfirm,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_AddExistingDialog> createState() => _AddExistingDialogState();
|
||||
}
|
||||
|
||||
class _AddExistingDialogState extends State<_AddExistingDialog> {
|
||||
String _filter = '';
|
||||
final _ctrl = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pt = PyramidTheme.of(context);
|
||||
final filtered = _filter.isEmpty
|
||||
? widget.rooms
|
||||
: widget.rooms
|
||||
.where((r) => r
|
||||
.getLocalizedDisplayname()
|
||||
.toLowerCase()
|
||||
.contains(_filter.toLowerCase()))
|
||||
.toList();
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: pt.bg1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rLg)),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 520),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text('Raum hinzufügen',
|
||||
style: TextStyle(
|
||||
color: pt.fg,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700)),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, color: pt.fgDim, size: 20),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_SearchField(
|
||||
controller: _ctrl,
|
||||
hint: 'Raum suchen…',
|
||||
pt: pt,
|
||||
onChanged: (v) => setState(() => _filter = v),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
if (filtered.isEmpty)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text('Keine Räume gefunden',
|
||||
style: TextStyle(color: pt.fgDim, fontSize: 13)),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (context, i) {
|
||||
final room = filtered[i];
|
||||
final name = room.getLocalizedDisplayname();
|
||||
final letter =
|
||||
name.isNotEmpty ? name[0].toUpperCase() : '?';
|
||||
final avatarUri = room.avatar;
|
||||
final createContent =
|
||||
room.getState('m.room.create')?.content;
|
||||
final isVoice =
|
||||
createContent?['type'] == _kVoiceChannelType;
|
||||
return ListTile(
|
||||
dense: true,
|
||||
leading: avatarUri != null
|
||||
? MxcAvatar(
|
||||
mxcUri: avatarUri,
|
||||
client: widget.client,
|
||||
size: 32,
|
||||
borderRadius: BorderRadius.circular(pt.rSm),
|
||||
placeholder: (_) => _InitialAvatar(
|
||||
letter: letter, pt: pt, size: 32),
|
||||
)
|
||||
: _InitialAvatar(letter: letter, pt: pt, size: 32),
|
||||
title: Text(name,
|
||||
style: TextStyle(
|
||||
color: pt.fg,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text(
|
||||
isVoice ? 'Sprachkanal' : 'Textkanal',
|
||||
style: TextStyle(color: pt.fgDim, fontSize: 11),
|
||||
),
|
||||
trailing:
|
||||
Icon(isVoice ? Icons.volume_up_outlined : Icons.tag,
|
||||
color: pt.fgDim, size: 16),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
widget.onConfirm(room.id);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user