diff --git a/wien_talks/wien_talks_flutter/lib/widgets/add_quote_fab.dart b/wien_talks/wien_talks_flutter/lib/widgets/add_quote_fab.dart index 054493b..28af240 100644 --- a/wien_talks/wien_talks_flutter/lib/widgets/add_quote_fab.dart +++ b/wien_talks/wien_talks_flutter/lib/widgets/add_quote_fab.dart @@ -1,17 +1,199 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:location/location.dart'; import 'package:wien_talks_client/wien_talks_client.dart'; import 'package:wien_talks_flutter/helper/funmap_mgr.dart'; +class QuoteDraft { + final String text; + final String? author; + const QuoteDraft({required this.text, this.author}); +} + +typedef QuoteSubmit = FutureOr Function(QuoteDraft draft); + +/// If Simon reads this I'm sorry class AddQuoteFab extends StatelessWidget { const AddQuoteFab({ super.key, + this.tooltip = 'Add quote', + this.icon = Icons.format_quote_rounded, + this.mini = false, }); + final String tooltip; + final IconData icon; + final bool mini; + + Future onSubmit(QuoteDraft draft) async { + final fix = await _getHackyLocation(); + if (fix == null || fix.latitude == null) { + return; + } + + await FunmapMgr().client.quote.createQuote( + CreateQuoteRequest( + text: draft.text, + authorName: draft.author, + lat: fix.latitude!, + lng: fix.longitude!, + ), + ); + } + + Future _getHackyLocation() async { + final loc = Location(); + + if (!await loc.serviceEnabled()) { + if (!await loc.requestService()) return null; + } + + var perm = await loc.hasPermission(); + if (perm == PermissionStatus.denied) { + perm = await loc.requestPermission(); + if (perm != PermissionStatus.granted) return null; + } + + return await loc.getLocation(); + } + @override Widget build(BuildContext context) { - return FloatingActionButton(onPressed: () { - FunmapMgr().client.quote.createQuote( - CreateQuoteRequest(text: 'Quote Text', lat: 22, lng: 140)); - }); + return FloatingActionButton( + mini: mini, + tooltip: tooltip, + onPressed: () async { + final draft = await showQuoteEditor(context); + if (draft != null) { + await onSubmit(draft); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Quote saved')), + ); + } + } + }, + child: Icon(icon), + ); } } + +Future showQuoteEditor( + BuildContext context, { + String? initialText, + String? initialAuthor, +}) async { + final textCtrl = TextEditingController(text: initialText ?? ''); + final authorCtrl = TextEditingController(text: initialAuthor ?? ''); + const maxChars = 500; + + return showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + showDragHandle: true, + builder: (ctx) { + bool canSave() { + final t = textCtrl.text.trim(); + return t.isNotEmpty && t.length <= maxChars; + } + + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(ctx).viewInsets.bottom, + ), + child: StatefulBuilder( + builder: (ctx, setSheetState) { + void onChanged(_) => setSheetState(() {}); + final remaining = maxChars - textCtrl.text.characters.length; + + return SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Text('New quote', style: Theme.of(ctx).textTheme.titleMedium), + const SizedBox(height: 8), + TextField( + controller: textCtrl, + onChanged: onChanged, + autofocus: true, + maxLines: null, + minLines: 3, + textInputAction: TextInputAction.newline, + decoration: InputDecoration( + labelText: 'Quote text', + hintText: 'How is Vienna surprising you today', + helperText: 'Max $maxChars characters', + counterText: + '${textCtrl.text.characters.length}/$maxChars', + border: const OutlineInputBorder(), + ), + maxLength: maxChars, + maxLengthEnforcement: MaxLengthEnforcement.enforced, + ), + const SizedBox(height: 12), + TextField( + controller: authorCtrl, + textInputAction: TextInputAction.done, + decoration: const InputDecoration( + labelText: 'Author (optional)', + hintText: 'e.g., Schmausi Wamperl', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.of(ctx).pop(null), + child: const Text('Cancel'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton.icon( + onPressed: canSave() + ? () => Navigator.of(ctx).pop( + QuoteDraft( + text: textCtrl.text.trim(), + author: authorCtrl.text.trim().isEmpty + ? null + : authorCtrl.text.trim(), + ), + ) + : null, + icon: const Icon(Icons.check), + label: const Text('Save'), + ), + ), + ], + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: Text( + remaining >= 0 + ? '$remaining characters left' + : '${-remaining} over limit', + style: Theme.of(ctx).textTheme.labelSmall?.copyWith( + color: Theme.of(ctx) + .colorScheme + .onSurface + .withValues(alpha: 0.7), + ), + ), + ), + ], + ), + ); + }, + ), + ); + }, + ); +} diff --git a/wien_talks/wien_talks_flutter/lib/widgets/screen_widget.dart b/wien_talks/wien_talks_flutter/lib/widgets/screen_widget.dart index 49613cd..d24fbc5 100644 --- a/wien_talks/wien_talks_flutter/lib/widgets/screen_widget.dart +++ b/wien_talks/wien_talks_flutter/lib/widgets/screen_widget.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:loader_overlay/loader_overlay.dart'; +import 'package:wien_talks_flutter/widgets/add_quote_fab.dart'; class ScreenWidget extends StatelessWidget { final Widget child; @@ -15,7 +16,16 @@ class ScreenWidget extends StatelessWidget { appBar: AppBar( title: const Text('FunMap'), ), -// floatingActionButton: AddQuoteFab(), + floatingActionButton: AddQuoteFab( + // onSubmit: (draft) { + // FunmapMgr().client.quote.createQuote(CreateQuoteRequest( + // text: draft.text, + // authorName: draft.author, + // lat: LocationMgr().lastLocation!.latitude!, + // lng: LocationMgr().lastLocation!.longitude!, + // )); + // }, + ), body: SafeArea( child: Padding( padding: const EdgeInsets.all(8.0), @@ -28,7 +38,8 @@ class ScreenWidget extends StatelessWidget { ); case 1: return Center( - child: SpinKitCubeGrid(size: 50, color: Theme.of(context).primaryColor), + child: SpinKitCubeGrid( + size: 50, color: Theme.of(context).primaryColor), ); case 2: return Center( @@ -36,15 +47,18 @@ class ScreenWidget extends StatelessWidget { ); case 3: return Center( - child: SpinKitHourGlass(color: Theme.of(context).primaryColor), + child: + SpinKitHourGlass(color: Theme.of(context).primaryColor), ); case 4: return Center( - child: SpinKitFadingCircle(color: Theme.of(context).primaryColor), + child: SpinKitFadingCircle( + color: Theme.of(context).primaryColor), ); default: return Center( - child: SpinKitPulsingGrid(color: Theme.of(context).primaryColor), + child: SpinKitPulsingGrid( + color: Theme.of(context).primaryColor), ); } },