implemented full interactive poll support in Talk, including creation, voting, and closing functionality
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/create_poll/create_poll.dart';
|
||||
import '../../../../api/marianumcloud/talk/create_poll/create_poll_params.dart';
|
||||
import '../../../../api/marianumcloud/talk/get_poll/get_poll_state_response.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
|
||||
/// Opens the poll creation form. On success the server posts the poll message
|
||||
/// into the chat itself, so the caller only needs to refresh via [onCreated].
|
||||
void showPollCreateSheet(
|
||||
BuildContext context, {
|
||||
required String token,
|
||||
required VoidCallback onCreated,
|
||||
}) {
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
header: const ListTile(
|
||||
leading: Icon(Icons.poll_outlined),
|
||||
title: Text('Umfrage erstellen'),
|
||||
),
|
||||
children: (sheetCtx) => [_PollCreateBody(token: token, onCreated: onCreated)],
|
||||
);
|
||||
}
|
||||
|
||||
class _PollCreateBody extends StatefulWidget {
|
||||
final String token;
|
||||
final VoidCallback onCreated;
|
||||
|
||||
const _PollCreateBody({required this.token, required this.onCreated});
|
||||
|
||||
@override
|
||||
State<_PollCreateBody> createState() => _PollCreateBodyState();
|
||||
}
|
||||
|
||||
class _PollCreateBodyState extends State<_PollCreateBody> {
|
||||
final TextEditingController _question = TextEditingController();
|
||||
final List<TextEditingController> _options = [
|
||||
TextEditingController(),
|
||||
TextEditingController(),
|
||||
];
|
||||
final AsyncActionController _submitController = AsyncActionController();
|
||||
bool _hideResults = false;
|
||||
bool _multipleChoice = false;
|
||||
String? _validationError;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_question.dispose();
|
||||
for (final controller in _options) {
|
||||
controller.dispose();
|
||||
}
|
||||
_submitController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _addOption() {
|
||||
setState(() => _options.add(TextEditingController()));
|
||||
}
|
||||
|
||||
void _removeOption(int index) {
|
||||
setState(() {
|
||||
_options.removeAt(index).dispose();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (guardDemoAction(context)) return;
|
||||
final question = _question.text.trim();
|
||||
final options = _options
|
||||
.map((c) => c.text.trim())
|
||||
.where((text) => text.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
if (question.isEmpty) {
|
||||
setState(() => _validationError = 'Bitte gib eine Frage ein.');
|
||||
return;
|
||||
}
|
||||
if (options.length < 2) {
|
||||
setState(
|
||||
() => _validationError = 'Bitte gib mindestens zwei Optionen an.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() => _validationError = null);
|
||||
|
||||
await CreatePoll(
|
||||
token: widget.token,
|
||||
params: CreatePollParams(
|
||||
question: question,
|
||||
options: options,
|
||||
resultMode: _hideResults
|
||||
? pollResultModeHidden
|
||||
: pollResultModePublic,
|
||||
maxVotes: _multipleChoice ? 0 : 1,
|
||||
),
|
||||
).run();
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
widget.onCreated();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: TextField(
|
||||
controller: _question,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Frage',
|
||||
hintText: 'Worüber soll abgestimmt werden?',
|
||||
),
|
||||
),
|
||||
),
|
||||
...List.generate(_options.length, (index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _options[index],
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Option ${index + 1}',
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Option entfernen',
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
onPressed: _options.length > 2
|
||||
? () => _removeOption(index)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton.icon(
|
||||
onPressed: _addOption,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Option hinzufügen'),
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: _hideResults,
|
||||
onChanged: (value) => setState(() => _hideResults = value),
|
||||
title: const Text('Ergebnisse bis zum Schließen verbergen'),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: _multipleChoice,
|
||||
onChanged: (value) => setState(() => _multipleChoice = value),
|
||||
title: const Text('Mehrfachauswahl erlauben'),
|
||||
),
|
||||
if (_validationError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
_validationError!,
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: AsyncActionButton(
|
||||
controller: _submitController,
|
||||
onPressed: _submit,
|
||||
child: const Text('Umfrage erstellen'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user