mirror of
https://github.com/timokz/flutter-vienna-hackathon-25.git
synced 2025-11-08 21:24:20 +01:00
introduce bottom modal for quote entry
This commit is contained in:
parent
39e4f1142f
commit
bf28ff429a
2 changed files with 205 additions and 9 deletions
|
|
@ -1,17 +1,199 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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_client/wien_talks_client.dart';
|
||||||
import 'package:wien_talks_flutter/helper/funmap_mgr.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<void> Function(QuoteDraft draft);
|
||||||
|
|
||||||
|
/// If Simon reads this I'm sorry
|
||||||
class AddQuoteFab extends StatelessWidget {
|
class AddQuoteFab extends StatelessWidget {
|
||||||
const AddQuoteFab({
|
const AddQuoteFab({
|
||||||
super.key,
|
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<void> 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<LocationData?> _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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FloatingActionButton(onPressed: () {
|
return FloatingActionButton(
|
||||||
FunmapMgr().client.quote.createQuote(
|
mini: mini,
|
||||||
CreateQuoteRequest(text: 'Quote Text', lat: 22, lng: 140));
|
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<QuoteDraft?> showQuoteEditor(
|
||||||
|
BuildContext context, {
|
||||||
|
String? initialText,
|
||||||
|
String? initialAuthor,
|
||||||
|
}) async {
|
||||||
|
final textCtrl = TextEditingController(text: initialText ?? '');
|
||||||
|
final authorCtrl = TextEditingController(text: initialAuthor ?? '');
|
||||||
|
const maxChars = 500;
|
||||||
|
|
||||||
|
return showModalBottomSheet<QuoteDraft>(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
import 'package:loader_overlay/loader_overlay.dart';
|
import 'package:loader_overlay/loader_overlay.dart';
|
||||||
|
import 'package:wien_talks_flutter/widgets/add_quote_fab.dart';
|
||||||
|
|
||||||
class ScreenWidget extends StatelessWidget {
|
class ScreenWidget extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
@ -15,7 +16,16 @@ class ScreenWidget extends StatelessWidget {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('FunMap'),
|
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(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
|
@ -28,7 +38,8 @@ class ScreenWidget extends StatelessWidget {
|
||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
return Center(
|
return Center(
|
||||||
child: SpinKitCubeGrid(size: 50, color: Theme.of(context).primaryColor),
|
child: SpinKitCubeGrid(
|
||||||
|
size: 50, color: Theme.of(context).primaryColor),
|
||||||
);
|
);
|
||||||
case 2:
|
case 2:
|
||||||
return Center(
|
return Center(
|
||||||
|
|
@ -36,15 +47,18 @@ class ScreenWidget extends StatelessWidget {
|
||||||
);
|
);
|
||||||
case 3:
|
case 3:
|
||||||
return Center(
|
return Center(
|
||||||
child: SpinKitHourGlass(color: Theme.of(context).primaryColor),
|
child:
|
||||||
|
SpinKitHourGlass(color: Theme.of(context).primaryColor),
|
||||||
);
|
);
|
||||||
case 4:
|
case 4:
|
||||||
return Center(
|
return Center(
|
||||||
child: SpinKitFadingCircle(color: Theme.of(context).primaryColor),
|
child: SpinKitFadingCircle(
|
||||||
|
color: Theme.of(context).primaryColor),
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return Center(
|
return Center(
|
||||||
child: SpinKitPulsingGrid(color: Theme.of(context).primaryColor),
|
child: SpinKitPulsingGrid(
|
||||||
|
color: Theme.of(context).primaryColor),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue