mirror of
https://github.com/timokz/flutter-vienna-hackathon-25.git
synced 2025-11-08 23:04:20 +01:00
Compare commits
2 commits
39e4f1142f
...
47cfb949ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47cfb949ac | ||
|
|
bf28ff429a |
13 changed files with 345 additions and 130 deletions
|
|
@ -47,12 +47,6 @@ class EndpointQuote extends _i1.EndpointRef {
|
||||||
@override
|
@override
|
||||||
String get name => 'quote';
|
String get name => 'quote';
|
||||||
|
|
||||||
_i2.Future<String> dbPing() => caller.callServerEndpoint<String>(
|
|
||||||
'quote',
|
|
||||||
'dbPing',
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
_i2.Future<_i4.Quote> createQuote(_i5.CreateQuoteRequest req) =>
|
_i2.Future<_i4.Quote> createQuote(_i5.CreateQuoteRequest req) =>
|
||||||
caller.callServerEndpoint<_i4.Quote>(
|
caller.callServerEndpoint<_i4.Quote>(
|
||||||
'quote',
|
'quote',
|
||||||
|
|
@ -73,6 +67,14 @@ class EndpointQuote extends _i1.EndpointRef {
|
||||||
'getAllQuotes',
|
'getAllQuotes',
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_i2.Stream<_i4.Quote> streamAllQuotes({required int limit}) =>
|
||||||
|
caller.callStreamingServerEndpoint<_i2.Stream<_i4.Quote>, _i4.Quote>(
|
||||||
|
'quote',
|
||||||
|
'streamAllQuotes',
|
||||||
|
{'limit': limit},
|
||||||
|
{},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// {@category Endpoint}
|
/// {@category Endpoint}
|
||||||
|
|
|
||||||
3
wien_talks/wien_talks_flutter/devtools_options.yaml
Normal file
3
wien_talks/wien_talks_flutter/devtools_options.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
|
|
@ -29,7 +29,7 @@ class FunmapMgr {
|
||||||
serverUrl =
|
serverUrl =
|
||||||
serverUrlFromEnv.isEmpty ? 'http://$localhost:8080/' : serverUrlFromEnv;
|
serverUrlFromEnv.isEmpty ? 'http://$localhost:8080/' : serverUrlFromEnv;
|
||||||
|
|
||||||
client = Client(serverUrl, connectionTimeout: const Duration(seconds: 5))
|
client = Client(serverUrl, connectionTimeout: const Duration(seconds: 2))
|
||||||
..connectivityMonitor = FlutterConnectivityMonitor();
|
..connectivityMonitor = FlutterConnectivityMonitor();
|
||||||
|
|
||||||
client.openStreamingConnection();
|
client.openStreamingConnection();
|
||||||
|
|
|
||||||
17
wien_talks/wien_talks_flutter/lib/helper/time_util.dart
Normal file
17
wien_talks/wien_talks_flutter/lib/helper/time_util.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Clanker code, I caved
|
||||||
|
String timeAgo(DateTime? dt) {
|
||||||
|
final d =
|
||||||
|
(dt ?? DateTime.fromMillisecondsSinceEpoch(0, isUtc: true)).toLocal();
|
||||||
|
final now = DateTime.now();
|
||||||
|
final diff = now.isBefore(d) ? Duration.zero : now.difference(d);
|
||||||
|
|
||||||
|
return switch (diff.inSeconds) {
|
||||||
|
< 60 => 'just now',
|
||||||
|
< 3600 => '${diff.inMinutes}m ago',
|
||||||
|
< 86400 => '${diff.inHours}h ago',
|
||||||
|
< 604800 => '${diff.inDays}d ago',
|
||||||
|
_ => '${d.year.toString().padLeft(4, '0')}-'
|
||||||
|
'${d.month.toString().padLeft(2, '0')}-'
|
||||||
|
'${d.day.toString().padLeft(2, '0')}',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,7 @@ class HomeScreen extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
IntroTextWidget(),
|
IntroTextWidget(),
|
||||||
SizedBox(height: 200, child: ShowLatestNewsWidget()),
|
SizedBox(height: 200, child: LatestQuotesScreen()),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 30,
|
height: 30,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class NewsScreen extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
HeadingText(text: "What's being said"),
|
HeadingText(text: "What's being said"),
|
||||||
ShowLatestNewsWidget(),
|
LatestQuotesScreen(),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 30,
|
height: 30,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -3,97 +3,91 @@ import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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';
|
||||||
|
import 'package:wien_talks_flutter/helper/time_util.dart';
|
||||||
import 'package:wien_talks_flutter/widgets/quote_card.dart';
|
import 'package:wien_talks_flutter/widgets/quote_card.dart';
|
||||||
|
|
||||||
class ShowLatestNewsWidget extends StatefulWidget {
|
class LatestQuotesScreen extends StatefulWidget {
|
||||||
const ShowLatestNewsWidget({super.key});
|
const LatestQuotesScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ShowLatestNewsWidget> createState() => _ShowLatestNewsWidgetState();
|
State<LatestQuotesScreen> createState() => _LatestQuotesScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ShowLatestNewsWidgetState extends State<ShowLatestNewsWidget> {
|
class _LatestQuotesScreenState extends State<LatestQuotesScreen> {
|
||||||
List<Quote>? _quotes;
|
final List<Quote> _quotes = [];
|
||||||
|
StreamSubscription<Quote>? _sub;
|
||||||
|
|
||||||
Object? _error;
|
Object? _error;
|
||||||
bool _loading = true;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_fetch();
|
_connectStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetch() async {
|
@override
|
||||||
setState(() {
|
void dispose() {
|
||||||
_loading = true;
|
_sub?.cancel();
|
||||||
_error = null;
|
super.dispose();
|
||||||
});
|
|
||||||
try {
|
|
||||||
final list = await FunmapMgr().client.quote.getAllQuotes();
|
|
||||||
final quotes = list.whereType<Quote>().toList(growable: false);
|
|
||||||
quotes.sort((a, b) => (b.createdAt).compareTo(a.createdAt));
|
|
||||||
setState(() {
|
|
||||||
_quotes = quotes;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
setState(() => _error = e);
|
|
||||||
} finally {
|
|
||||||
if (mounted) setState(() => _loading = false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _refresh() => _fetch();
|
void _connectStream() {
|
||||||
|
_sub?.cancel();
|
||||||
|
_sub = FunmapMgr().client.quote.streamAllQuotes(limit: 50).listen(
|
||||||
|
(q) => setState(() => _upsert(q)),
|
||||||
|
onError: (e) => setState(() => _error = e),
|
||||||
|
onDone: () => Future.delayed(const Duration(seconds: 2), () {
|
||||||
|
if (mounted) _connectStream();
|
||||||
|
}),
|
||||||
|
cancelOnError: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _upsert(Quote q) {
|
||||||
|
final i = _quotes.indexWhere((x) => x.id == q.id);
|
||||||
|
if (i >= 0)
|
||||||
|
_quotes[i] = q;
|
||||||
|
else
|
||||||
|
_quotes.add(q);
|
||||||
|
_quotes.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sortDesc() {
|
||||||
|
_quotes.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _vote(Quote quote, bool up) async {
|
Future<void> _vote(Quote quote, bool up) async {
|
||||||
if (_quotes == null) return;
|
final idx = _quotes.indexWhere((q) => q.id == quote.id);
|
||||||
final idx = _quotes!.indexWhere((q) => q.id == quote.id);
|
|
||||||
if (idx < 0) return;
|
if (idx < 0) return;
|
||||||
|
|
||||||
final original = _quotes![idx];
|
final original = _quotes[idx];
|
||||||
final updated = original.copyWith(
|
final updated = original.copyWith(
|
||||||
upvotes: up ? original.upvotes + 1 : original.upvotes,
|
upvotes: up ? original.upvotes + 1 : original.upvotes,
|
||||||
downvotes: up ? original.downvotes : original.downvotes + 1,
|
downvotes: up ? original.downvotes : original.downvotes + 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
final copy = List<Quote>.from(_quotes!);
|
_quotes[idx] = updated;
|
||||||
copy[idx] = updated;
|
_sortDesc();
|
||||||
_quotes = copy;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await FunmapMgr().client.quote.updateQuote(updated);
|
await FunmapMgr().client.quote.updateQuote(updated);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() {
|
|
||||||
final copy = List<Quote>.from(_quotes!);
|
|
||||||
copy[idx] = original;
|
|
||||||
_quotes = copy;
|
|
||||||
});
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
setState(() => _quotes[idx] = original);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Vote failed: $e')),
|
SnackBar(content: Text('Vote failed: $e')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _timeAgo(DateTime? dt) {
|
|
||||||
final d = (dt ?? DateTime.fromMillisecondsSinceEpoch(0)).toLocal();
|
|
||||||
final diff = DateTime.now().difference(d);
|
|
||||||
if (diff.inSeconds < 60) return 'just now';
|
|
||||||
if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
|
|
||||||
if (diff.inHours < 24) return '${diff.inHours}h ago';
|
|
||||||
if (diff.inDays < 7) return '${diff.inDays}d ago';
|
|
||||||
final m = d.month.toString().padLeft(2, '0');
|
|
||||||
final day = d.day.toString().padLeft(2, '0');
|
|
||||||
return '${d.year}-$m-$day';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_loading) {
|
if (_quotes.isEmpty && _error == null) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
if (_error != null) {
|
if (_error != null && _quotes.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|
@ -101,9 +95,8 @@ class _ShowLatestNewsWidgetState extends State<ShowLatestNewsWidget> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final quotes = _quotes ?? const <Quote>[];
|
if (_quotes.isEmpty) {
|
||||||
if (quotes.isEmpty) {
|
return const Center(child: Text('Nix da. Sag halt was'));
|
||||||
return const Center(child: Text('No quotes yet.'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
|
|
@ -116,14 +109,14 @@ class _ShowLatestNewsWidgetState extends State<ShowLatestNewsWidget> {
|
||||||
physics: unboundedHeight
|
physics: unboundedHeight
|
||||||
? const NeverScrollableScrollPhysics()
|
? const NeverScrollableScrollPhysics()
|
||||||
: const AlwaysScrollableScrollPhysics(),
|
: const AlwaysScrollableScrollPhysics(),
|
||||||
itemCount: quotes.length,
|
itemCount: _quotes.length,
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
final q = quotes[i];
|
final q = _quotes[i];
|
||||||
final author = (q.authorName ?? '').trim();
|
final author = (q.authorName ?? '').trim();
|
||||||
final meta = [
|
final meta = [
|
||||||
if (author.isNotEmpty) author,
|
if (author.isNotEmpty) author,
|
||||||
_timeAgo(q.createdAt),
|
timeAgo(q.createdAt),
|
||||||
].join(' · ');
|
].join(' · ');
|
||||||
|
|
||||||
return QuoteCard(
|
return QuoteCard(
|
||||||
|
|
@ -135,8 +128,9 @@ class _ShowLatestNewsWidgetState extends State<ShowLatestNewsWidget> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (unboundedHeight) return list;
|
return unboundedHeight
|
||||||
return RefreshIndicator(onRefresh: _refresh, child: list);
|
? list
|
||||||
|
: RefreshIndicator(onRefresh: () async {}, child: list);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: 'Vienna`s finest',
|
||||||
|
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,7 @@ class ScreenWidget extends StatelessWidget {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('FunMap'),
|
title: const Text('FunMap'),
|
||||||
),
|
),
|
||||||
// floatingActionButton: AddQuoteFab(),
|
floatingActionButton: AddQuoteFab(),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
|
@ -28,7 +29,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 +38,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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -77,15 +77,6 @@ class Endpoints extends _i1.EndpointDispatch {
|
||||||
name: 'quote',
|
name: 'quote',
|
||||||
endpoint: endpoints['quote']!,
|
endpoint: endpoints['quote']!,
|
||||||
methodConnectors: {
|
methodConnectors: {
|
||||||
'dbPing': _i1.MethodConnector(
|
|
||||||
name: 'dbPing',
|
|
||||||
params: {},
|
|
||||||
call: (
|
|
||||||
_i1.Session session,
|
|
||||||
Map<String, dynamic> params,
|
|
||||||
) async =>
|
|
||||||
(endpoints['quote'] as _i3.QuoteEndpoint).dbPing(session),
|
|
||||||
),
|
|
||||||
'createQuote': _i1.MethodConnector(
|
'createQuote': _i1.MethodConnector(
|
||||||
name: 'createQuote',
|
name: 'createQuote',
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -131,6 +122,27 @@ class Endpoints extends _i1.EndpointDispatch {
|
||||||
) async =>
|
) async =>
|
||||||
(endpoints['quote'] as _i3.QuoteEndpoint).getAllQuotes(session),
|
(endpoints['quote'] as _i3.QuoteEndpoint).getAllQuotes(session),
|
||||||
),
|
),
|
||||||
|
'streamAllQuotes': _i1.MethodStreamConnector(
|
||||||
|
name: 'streamAllQuotes',
|
||||||
|
params: {
|
||||||
|
'limit': _i1.ParameterDescription(
|
||||||
|
name: 'limit',
|
||||||
|
type: _i1.getType<int>(),
|
||||||
|
nullable: false,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
streamParams: {},
|
||||||
|
returnType: _i1.MethodStreamReturnType.streamType,
|
||||||
|
call: (
|
||||||
|
_i1.Session session,
|
||||||
|
Map<String, dynamic> params,
|
||||||
|
Map<String, Stream> streamParams,
|
||||||
|
) =>
|
||||||
|
(endpoints['quote'] as _i3.QuoteEndpoint).streamAllQuotes(
|
||||||
|
session,
|
||||||
|
limit: params['limit'],
|
||||||
|
),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
connectors['votes'] = _i1.EndpointConnector(
|
connectors['votes'] = _i1.EndpointConnector(
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ health:
|
||||||
- ping:
|
- ping:
|
||||||
- all:
|
- all:
|
||||||
quote:
|
quote:
|
||||||
- dbPing:
|
|
||||||
- createQuote:
|
- createQuote:
|
||||||
- updateQuote:
|
- updateQuote:
|
||||||
- getAllQuotes:
|
- getAllQuotes:
|
||||||
|
- streamAllQuotes:
|
||||||
votes:
|
votes:
|
||||||
- getAllVotes:
|
- getAllVotes:
|
||||||
- createVote:
|
- createVote:
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,6 @@ import 'package:wien_talks_server/src/quotes/quote_util.dart';
|
||||||
class QuoteEndpoint extends Endpoint {
|
class QuoteEndpoint extends Endpoint {
|
||||||
static const _channelQuoteUpdates = 'quote-updates';
|
static const _channelQuoteUpdates = 'quote-updates';
|
||||||
|
|
||||||
Future<String> dbPing(Session session) async {
|
|
||||||
await session.db.unsafeQuery('SELECT 1;'); // connectivity
|
|
||||||
await session.db
|
|
||||||
.unsafeQuery('SELECT 1 FROM public.quote LIMIT 1;'); // table visible
|
|
||||||
return 'ok';
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Quote> createQuote(Session session, CreateQuoteRequest req) async {
|
Future<Quote> createQuote(Session session, CreateQuoteRequest req) async {
|
||||||
final authInfo = await session.authenticated;
|
final authInfo = await session.authenticated;
|
||||||
final userId = authInfo?.userId;
|
final userId = authInfo?.userId;
|
||||||
|
|
@ -50,7 +43,6 @@ class QuoteEndpoint extends Endpoint {
|
||||||
|
|
||||||
final quoteList = await Quote.db.find(
|
final quoteList = await Quote.db.find(
|
||||||
session,
|
session,
|
||||||
// where: (t) => t.visibility.equals(0),
|
|
||||||
orderBy: (t) => t.createdAt,
|
orderBy: (t) => t.createdAt,
|
||||||
orderDescending: true,
|
orderDescending: true,
|
||||||
);
|
);
|
||||||
|
|
@ -62,13 +54,12 @@ class QuoteEndpoint extends Endpoint {
|
||||||
return quoteList;
|
return quoteList;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Stream<Quote>> streamAllQuotes(StreamingSession session,
|
Stream<Quote> streamAllQuotes(Session session, {int limit = 50}) {
|
||||||
{int limit = 200}) async {
|
|
||||||
if (limit <= 0 || limit > 500) limit = 200;
|
|
||||||
|
|
||||||
final controller = StreamController<Quote>();
|
final controller = StreamController<Quote>();
|
||||||
final live = session.messages.createStream<Quote>(_channelQuoteUpdates);
|
final live = session.messages.createStream<Quote>(_channelQuoteUpdates);
|
||||||
final liveSub = live.listen(
|
|
||||||
|
StreamSubscription<Quote>? sub;
|
||||||
|
sub = live.listen(
|
||||||
(q) {
|
(q) {
|
||||||
if (q.visibility == 0) controller.add(q);
|
if (q.visibility == 0) controller.add(q);
|
||||||
},
|
},
|
||||||
|
|
@ -76,10 +67,9 @@ class QuoteEndpoint extends Endpoint {
|
||||||
onDone: () {
|
onDone: () {
|
||||||
if (!controller.isClosed) controller.close();
|
if (!controller.isClosed) controller.close();
|
||||||
},
|
},
|
||||||
cancelOnError: false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
() async* {
|
() async {
|
||||||
try {
|
try {
|
||||||
final snapshot = await Quote.db.find(
|
final snapshot = await Quote.db.find(
|
||||||
session,
|
session,
|
||||||
|
|
@ -89,18 +79,22 @@ class QuoteEndpoint extends Endpoint {
|
||||||
limit: limit,
|
limit: limit,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
for (final q in snapshot.reversed) {
|
for (final q in snapshot.reversed) {
|
||||||
controller.add(q);
|
controller.add(q);
|
||||||
|
if ((++i % 25) == 0) {
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
controller.addError(e, st);
|
controller.addError(e, st);
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
|
||||||
await session.close().then((_) async {
|
controller.onCancel = () async {
|
||||||
await liveSub.cancel();
|
await sub?.cancel();
|
||||||
await controller.close();
|
await controller.close();
|
||||||
});
|
};
|
||||||
|
|
||||||
return controller.stream;
|
return controller.stream;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -210,32 +210,6 @@ class _QuoteEndpoint {
|
||||||
|
|
||||||
final _i2.SerializationManager _serializationManager;
|
final _i2.SerializationManager _serializationManager;
|
||||||
|
|
||||||
_i3.Future<String> dbPing(_i1.TestSessionBuilder sessionBuilder) async {
|
|
||||||
return _i1.callAwaitableFunctionAndHandleExceptions(() async {
|
|
||||||
var _localUniqueSession =
|
|
||||||
(sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild(
|
|
||||||
endpoint: 'quote',
|
|
||||||
method: 'dbPing',
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
|
||||||
createSessionCallback: (_) => _localUniqueSession,
|
|
||||||
endpointPath: 'quote',
|
|
||||||
methodName: 'dbPing',
|
|
||||||
parameters: _i1.testObjectToJson({}),
|
|
||||||
serializationManager: _serializationManager,
|
|
||||||
);
|
|
||||||
var _localReturnValue = await (_localCallContext.method.call(
|
|
||||||
_localUniqueSession,
|
|
||||||
_localCallContext.arguments,
|
|
||||||
) as _i3.Future<String>);
|
|
||||||
return _localReturnValue;
|
|
||||||
} finally {
|
|
||||||
await _localUniqueSession.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_i3.Future<_i5.Quote> createQuote(
|
_i3.Future<_i5.Quote> createQuote(
|
||||||
_i1.TestSessionBuilder sessionBuilder,
|
_i1.TestSessionBuilder sessionBuilder,
|
||||||
_i6.CreateQuoteRequest req,
|
_i6.CreateQuoteRequest req,
|
||||||
|
|
@ -320,6 +294,38 @@ class _QuoteEndpoint {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_i3.Stream<_i5.Quote> streamAllQuotes(
|
||||||
|
_i1.TestSessionBuilder sessionBuilder, {
|
||||||
|
required int limit,
|
||||||
|
}) {
|
||||||
|
var _localTestStreamManager = _i1.TestStreamManager<_i5.Quote>();
|
||||||
|
_i1.callStreamFunctionAndHandleExceptions(
|
||||||
|
() async {
|
||||||
|
var _localUniqueSession =
|
||||||
|
(sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild(
|
||||||
|
endpoint: 'quote',
|
||||||
|
method: 'streamAllQuotes',
|
||||||
|
);
|
||||||
|
var _localCallContext =
|
||||||
|
await _endpointDispatch.getMethodStreamCallContext(
|
||||||
|
createSessionCallback: (_) => _localUniqueSession,
|
||||||
|
endpointPath: 'quote',
|
||||||
|
methodName: 'streamAllQuotes',
|
||||||
|
arguments: {'limit': limit},
|
||||||
|
requestedInputStreams: [],
|
||||||
|
serializationManager: _serializationManager,
|
||||||
|
);
|
||||||
|
await _localTestStreamManager.callStreamMethod(
|
||||||
|
_localCallContext,
|
||||||
|
_localUniqueSession,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
_localTestStreamManager.outputStreamController,
|
||||||
|
);
|
||||||
|
return _localTestStreamManager.outputStreamController.stream;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VotesEndpoint {
|
class _VotesEndpoint {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue