mirror of
https://github.com/timokz/flutter-vienna-hackathon-25.git
synced 2025-11-08 19:04:20 +01:00
restyle quote list
This commit is contained in:
parent
595b4e730e
commit
39e4f1142f
17 changed files with 394 additions and 107 deletions
|
|
@ -10,7 +10,7 @@ class FunmapMgr {
|
||||||
|
|
||||||
late Client client;
|
late Client client;
|
||||||
|
|
||||||
late final serverUrl;
|
late final String serverUrl;
|
||||||
|
|
||||||
factory FunmapMgr() {
|
factory FunmapMgr() {
|
||||||
if (_instance != null) return _instance!;
|
if (_instance != null) return _instance!;
|
||||||
|
|
@ -26,9 +26,11 @@ class FunmapMgr {
|
||||||
// E.g. `flutter run --dart-define=SERVER_URL=https://api.example.com/`
|
// E.g. `flutter run --dart-define=SERVER_URL=https://api.example.com/`
|
||||||
|
|
||||||
const serverUrlFromEnv = String.fromEnvironment('SERVER_URL');
|
const serverUrlFromEnv = String.fromEnvironment('SERVER_URL');
|
||||||
serverUrl = serverUrlFromEnv.isEmpty ? 'http://$localhost:8080/' : serverUrlFromEnv;
|
serverUrl =
|
||||||
|
serverUrlFromEnv.isEmpty ? 'http://$localhost:8080/' : serverUrlFromEnv;
|
||||||
|
|
||||||
client = Client(serverUrl, connectionTimeout: const Duration(seconds: 5))..connectivityMonitor = FlutterConnectivityMonitor();
|
client = Client(serverUrl, connectionTimeout: const Duration(seconds: 5))
|
||||||
|
..connectivityMonitor = FlutterConnectivityMonitor();
|
||||||
|
|
||||||
client.openStreamingConnection();
|
client.openStreamingConnection();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:wien_talks_flutter/create_event_screen.dart';
|
import 'package:wien_talks_flutter/screens/create_event_screen.dart';
|
||||||
import 'package:wien_talks_flutter/helper/auth_service.dart';
|
import 'package:wien_talks_flutter/screens/login_page.dart';
|
||||||
import 'package:wien_talks_flutter/login_page.dart';
|
import 'package:wien_talks_flutter/screens/news_screen.dart';
|
||||||
import 'package:wien_talks_flutter/news_screen.dart';
|
|
||||||
|
|
||||||
final router = GoRouter(
|
final router = GoRouter(
|
||||||
redirect: (context, state) {
|
|
||||||
final loggedIn = true; // AuthService.user != null;
|
|
||||||
final atLogin = state.matchedLocation == '/login';
|
|
||||||
if (!loggedIn && !atLogin) return '/login';
|
|
||||||
if (loggedIn && atLogin) return '/';
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
refreshListenable: AuthChangeNotifier(),
|
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(path: '/login', builder: (c, s) => const LoginScreen()),
|
GoRoute(path: '/login', builder: (c, s) => const LoginScreen()),
|
||||||
GoRoute(path: '/', builder: (c, s) => NewsScreen()),
|
GoRoute(path: '/', builder: (c, s) => NewsScreen()),
|
||||||
|
|
@ -23,9 +13,3 @@ final router = GoRouter(
|
||||||
builder: (c, s) => CreateEventScreen()),
|
builder: (c, s) => CreateEventScreen()),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
class AuthChangeNotifier extends ChangeNotifier {
|
|
||||||
AuthChangeNotifier() {
|
|
||||||
AuthService.onUserChanged.listen((_) => notifyListeners());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ class LocationMgr {
|
||||||
|
|
||||||
final SymbolCache symbolCache = FileSymbolCache();
|
final SymbolCache symbolCache = FileSymbolCache();
|
||||||
|
|
||||||
final JobRenderer jobRenderer = kIsWeb ? MapOnlineRendererWeb() : MapOnlineRenderer();
|
final JobRenderer jobRenderer =
|
||||||
|
kIsWeb ? MapOnlineRendererWeb() : MapOnlineRenderer();
|
||||||
|
|
||||||
final MarkerByItemDataStore markerDataStore = MarkerByItemDataStore();
|
final MarkerByItemDataStore markerDataStore = MarkerByItemDataStore();
|
||||||
|
|
||||||
|
|
@ -44,7 +45,7 @@ class LocationMgr {
|
||||||
return _instance!;
|
return _instance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationMgr._() {}
|
LocationMgr._();
|
||||||
|
|
||||||
Future<String?> startup() async {
|
Future<String?> startup() async {
|
||||||
serviceEnabled = await location.serviceEnabled();
|
serviceEnabled = await location.serviceEnabled();
|
||||||
|
|
@ -70,18 +71,23 @@ class LocationMgr {
|
||||||
);
|
);
|
||||||
mapModel?.markerDataStores.add(markerDataStore);
|
mapModel?.markerDataStores.add(markerDataStore);
|
||||||
viewModel = ViewModel(displayModel: displayModel);
|
viewModel = ViewModel(displayModel: displayModel);
|
||||||
_subscription = location.onLocationChanged.listen((LocationData currentLocation) {
|
_subscription =
|
||||||
|
location.onLocationChanged.listen((LocationData currentLocation) {
|
||||||
_lastLocationData = currentLocation;
|
_lastLocationData = currentLocation;
|
||||||
if (currentLocation.latitude != null && currentLocation.longitude != null) {
|
if (currentLocation.latitude != null &&
|
||||||
viewModel?.setMapViewPosition(currentLocation.latitude!, currentLocation.longitude!);
|
currentLocation.longitude != null) {
|
||||||
|
viewModel?.setMapViewPosition(
|
||||||
|
currentLocation.latitude!, currentLocation.longitude!);
|
||||||
if (iconMarker == null) {
|
if (iconMarker == null) {
|
||||||
iconMarker ??= IconMarker(
|
iconMarker ??= IconMarker(
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
icon: Icons.gps_fixed,
|
icon: Icons.gps_fixed,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
center: LatLong(currentLocation.latitude!, currentLocation.longitude!),
|
center: LatLong(
|
||||||
|
currentLocation.latitude!, currentLocation.longitude!),
|
||||||
displayModel: displayModel);
|
displayModel: displayModel);
|
||||||
mapModel?.markerDataStores.add(MarkerDataStore()..addMarker(iconMarker!));
|
mapModel?.markerDataStores
|
||||||
|
.add(MarkerDataStore()..addMarker(iconMarker!));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_subject.add(currentLocation);
|
_subject.add(currentLocation);
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:mapsforge_flutter/core.dart';
|
import 'package:mapsforge_flutter/core.dart';
|
||||||
import 'package:wien_talks_flutter/location_mgr.dart';
|
import 'package:wien_talks_flutter/helper/location_mgr.dart';
|
||||||
|
|
||||||
class MapfileWidget extends StatefulWidget {
|
class MapfileWidget extends StatefulWidget {
|
||||||
const MapfileWidget({super.key});
|
const MapfileWidget({super.key});
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:location/location.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/get_location_widget.dart';
|
import 'package:wien_talks_flutter/widgets/get_location_widget.dart';
|
||||||
import 'package:wien_talks_flutter/helper/funmap_mgr.dart';
|
import 'package:wien_talks_flutter/helper/funmap_mgr.dart';
|
||||||
import 'package:wien_talks_flutter/mapfile_widget.dart';
|
import 'package:wien_talks_flutter/mapfile_widget.dart';
|
||||||
import 'package:wien_talks_flutter/news_input_form.dart';
|
import 'package:wien_talks_flutter/widgets/news_input_form.dart';
|
||||||
import 'package:wien_talks_flutter/widgets/screen_widget.dart';
|
import 'package:wien_talks_flutter/widgets/screen_widget.dart';
|
||||||
|
|
||||||
import 'location_mgr.dart';
|
import '../helper/location_mgr.dart';
|
||||||
|
|
||||||
class CreateEventScreen extends StatelessWidget {
|
class CreateEventScreen extends StatelessWidget {
|
||||||
const CreateEventScreen({super.key});
|
const CreateEventScreen({super.key});
|
||||||
|
|
@ -24,7 +24,11 @@ class CreateEventScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
StreamBuilder(
|
StreamBuilder(
|
||||||
stream: LocationMgr().stream,
|
stream: LocationMgr().stream,
|
||||||
builder: (BuildContext context, AsyncSnapshot<LocationData> snapshot) => snapshot.data != null ? Text(snapshot.data.toString()) : SizedBox()),
|
builder:
|
||||||
|
(BuildContext context, AsyncSnapshot<LocationData> snapshot) =>
|
||||||
|
snapshot.data != null
|
||||||
|
? Text(snapshot.data.toString())
|
||||||
|
: SizedBox()),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GetLocationWidget(
|
child: GetLocationWidget(
|
||||||
child: MapfileWidget(),
|
child: MapfileWidget(),
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:wien_talks_flutter/helper/funmap_mgr.dart';
|
import 'package:wien_talks_flutter/helper/funmap_mgr.dart';
|
||||||
import 'package:wien_talks_flutter/show_latest_news_widget.dart';
|
import 'package:wien_talks_flutter/screens/show_latest_news_widget.dart';
|
||||||
import 'package:wien_talks_flutter/widgets/intro_text_widget.dart';
|
import 'package:wien_talks_flutter/widgets/intro_text_widget.dart';
|
||||||
import 'package:wien_talks_flutter/widgets/screen_widget.dart';
|
import 'package:wien_talks_flutter/widgets/screen_widget.dart';
|
||||||
|
|
||||||
import 'carousel_widget.dart';
|
import '../widgets/carousel_widget.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatelessWidget {
|
class HomeScreen extends StatelessWidget {
|
||||||
const HomeScreen({
|
const HomeScreen({
|
||||||
|
|
@ -29,8 +29,10 @@ class HomeScreen extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: WidgetStateProperty.all(Theme.of(context).primaryColor),
|
backgroundColor: WidgetStateProperty.all(
|
||||||
foregroundColor: WidgetStateProperty.all(Theme.of(context).colorScheme.onPrimary)),
|
Theme.of(context).primaryColor),
|
||||||
|
foregroundColor: WidgetStateProperty.all(
|
||||||
|
Theme.of(context).colorScheme.onPrimary)),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushNamed("create_event");
|
context.pushNamed("create_event");
|
||||||
},
|
},
|
||||||
|
|
@ -45,7 +47,8 @@ class HomeScreen extends StatelessWidget {
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Spacer(),
|
Spacer(),
|
||||||
Text(FunmapMgr().serverUrl, style: Theme.of(context).textTheme.bodySmall),
|
Text(FunmapMgr().serverUrl,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:wien_talks_flutter/show_latest_news_widget.dart';
|
import 'package:wien_talks_flutter/screens/show_latest_news_widget.dart';
|
||||||
import 'package:wien_talks_flutter/widgets/heading_text.dart';
|
import 'package:wien_talks_flutter/widgets/heading_text.dart';
|
||||||
import 'package:wien_talks_flutter/widgets/screen_widget.dart';
|
import 'package:wien_talks_flutter/widgets/screen_widget.dart';
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ class NewsScreen extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
HeadingText(text: "Latest news"),
|
HeadingText(text: "What's being said"),
|
||||||
ShowLatestNewsWidget(),
|
ShowLatestNewsWidget(),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 30,
|
height: 30,
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:wien_talks_client/wien_talks_client.dart';
|
||||||
|
import 'package:wien_talks_flutter/helper/funmap_mgr.dart';
|
||||||
|
import 'package:wien_talks_flutter/widgets/quote_card.dart';
|
||||||
|
|
||||||
|
class ShowLatestNewsWidget extends StatefulWidget {
|
||||||
|
const ShowLatestNewsWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShowLatestNewsWidget> createState() => _ShowLatestNewsWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShowLatestNewsWidgetState extends State<ShowLatestNewsWidget> {
|
||||||
|
List<Quote>? _quotes;
|
||||||
|
Object? _error;
|
||||||
|
bool _loading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetch() async {
|
||||||
|
setState(() {
|
||||||
|
_loading = true;
|
||||||
|
_error = null;
|
||||||
|
});
|
||||||
|
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();
|
||||||
|
|
||||||
|
Future<void> _vote(Quote quote, bool up) async {
|
||||||
|
if (_quotes == null) return;
|
||||||
|
final idx = _quotes!.indexWhere((q) => q.id == quote.id);
|
||||||
|
if (idx < 0) return;
|
||||||
|
|
||||||
|
final original = _quotes![idx];
|
||||||
|
final updated = original.copyWith(
|
||||||
|
upvotes: up ? original.upvotes + 1 : original.upvotes,
|
||||||
|
downvotes: up ? original.downvotes : original.downvotes + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
final copy = List<Quote>.from(_quotes!);
|
||||||
|
copy[idx] = updated;
|
||||||
|
_quotes = copy;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await FunmapMgr().client.quote.updateQuote(updated);
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
final copy = List<Quote>.from(_quotes!);
|
||||||
|
copy[idx] = original;
|
||||||
|
_quotes = copy;
|
||||||
|
});
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
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
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_loading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (_error != null) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text('Error: $_error'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final quotes = _quotes ?? const <Quote>[];
|
||||||
|
if (quotes.isEmpty) {
|
||||||
|
return const Center(child: Text('No quotes yet.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final unboundedHeight = constraints.maxHeight == double.infinity;
|
||||||
|
|
||||||
|
final list = ListView.separated(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
shrinkWrap: unboundedHeight,
|
||||||
|
physics: unboundedHeight
|
||||||
|
? const NeverScrollableScrollPhysics()
|
||||||
|
: const AlwaysScrollableScrollPhysics(),
|
||||||
|
itemCount: quotes.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
final q = quotes[i];
|
||||||
|
final author = (q.authorName ?? '').trim();
|
||||||
|
final meta = [
|
||||||
|
if (author.isNotEmpty) author,
|
||||||
|
_timeAgo(q.createdAt),
|
||||||
|
].join(' · ');
|
||||||
|
|
||||||
|
return QuoteCard(
|
||||||
|
quote: q,
|
||||||
|
meta: meta,
|
||||||
|
onVoteUp: () => _vote(q, true),
|
||||||
|
onVoteDown: () => _vote(q, false),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (unboundedHeight) return list;
|
||||||
|
return RefreshIndicator(onRefresh: _refresh, child: list);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:wien_talks_client/wien_talks_client.dart';
|
|
||||||
import 'package:wien_talks_flutter/helper/funmap_mgr.dart';
|
|
||||||
|
|
||||||
class ShowLatestNewsWidget extends StatelessWidget {
|
|
||||||
const ShowLatestNewsWidget({super.key});
|
|
||||||
|
|
||||||
Future<List<Quote>> _load() async {
|
|
||||||
final list = await FunmapMgr().client.quote.getAllQuotes();
|
|
||||||
return list.whereType<Quote>().toList(growable: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FutureBuilder<List<Quote>>(
|
|
||||||
future: _load(),
|
|
||||||
builder: (context, snap) {
|
|
||||||
if (snap.connectionState != ConnectionState.done) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
if (snap.hasError) {
|
|
||||||
return Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Text('Error: ${snap.error}'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final quotes = snap.data ?? const <Quote>[];
|
|
||||||
if (quotes.isEmpty) {
|
|
||||||
return const Center(child: Text('No quotes yet.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.separated(
|
|
||||||
itemCount: quotes.length,
|
|
||||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
|
||||||
itemBuilder: (context, i) {
|
|
||||||
final q = quotes[i];
|
|
||||||
final author = (q.authorName ?? '').trim();
|
|
||||||
final when = (q.createdAt ?? DateTime.fromMillisecondsSinceEpoch(0))
|
|
||||||
.toLocal()
|
|
||||||
.toString();
|
|
||||||
return ListTile(
|
|
||||||
title: Text(q.text),
|
|
||||||
subtitle: Text([
|
|
||||||
if (author.isNotEmpty) author,
|
|
||||||
when,
|
|
||||||
].join(' · ')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:wien_talks_flutter/location_mgr.dart';
|
import 'package:wien_talks_flutter/helper/location_mgr.dart';
|
||||||
|
|
||||||
class GetLocationWidget extends StatefulWidget {
|
class GetLocationWidget extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
@ -31,7 +31,8 @@ class _GetLocationWidgetState extends State<GetLocationWidget> {
|
||||||
{
|
{
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
// Error occured
|
// Error occured
|
||||||
return Text(snapshot.data.toString(), style: TextStyle(color: Colors.red));
|
return Text(snapshot.data.toString(),
|
||||||
|
style: TextStyle(color: Colors.red));
|
||||||
} else {
|
} else {
|
||||||
return widget.child;
|
return widget.child;
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:loader_overlay/loader_overlay.dart';
|
import 'package:loader_overlay/loader_overlay.dart';
|
||||||
import 'package:location/location.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/location_mgr.dart';
|
import 'package:wien_talks_flutter/helper/location_mgr.dart';
|
||||||
import 'package:wien_talks_flutter/widgets/error_snackbar.dart';
|
import 'package:wien_talks_flutter/widgets/error_snackbar.dart';
|
||||||
|
|
||||||
typedef OnSubmit = Future<void> Function(CreateQuoteRequest request);
|
typedef OnSubmit = Future<void> Function(CreateQuoteRequest request);
|
||||||
|
|
@ -28,8 +28,11 @@ class _NewsInputFormState extends State<NewsInputForm> {
|
||||||
|
|
||||||
void _submitForm() async {
|
void _submitForm() async {
|
||||||
LocationData? locationData = LocationMgr().lastLocation;
|
LocationData? locationData = LocationMgr().lastLocation;
|
||||||
if (locationData == null || locationData.latitude == null || locationData.longitude == null) {
|
if (locationData == null ||
|
||||||
ErrorSnackbar().show(context, "No location available, please retry later");
|
locationData.latitude == null ||
|
||||||
|
locationData.longitude == null) {
|
||||||
|
ErrorSnackbar()
|
||||||
|
.show(context, "No location available, please retry later");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
|
|
@ -79,8 +82,10 @@ class _NewsInputFormState extends State<NewsInputForm> {
|
||||||
const SizedBox(height: 16.0),
|
const SizedBox(height: 16.0),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: WidgetStateProperty.all(Theme.of(context).primaryColor),
|
backgroundColor:
|
||||||
foregroundColor: WidgetStateProperty.all(Theme.of(context).colorScheme.onPrimary)),
|
WidgetStateProperty.all(Theme.of(context).primaryColor),
|
||||||
|
foregroundColor: WidgetStateProperty.all(
|
||||||
|
Theme.of(context).colorScheme.onPrimary)),
|
||||||
onPressed: _submitForm,
|
onPressed: _submitForm,
|
||||||
child: const Text('Submit News'),
|
child: const Text('Submit News'),
|
||||||
),
|
),
|
||||||
121
wien_talks/wien_talks_flutter/lib/widgets/quote_card.dart
Normal file
121
wien_talks/wien_talks_flutter/lib/widgets/quote_card.dart
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:wien_talks_client/wien_talks_client.dart';
|
||||||
|
import 'package:wien_talks_flutter/widgets/vote_button.dart';
|
||||||
|
|
||||||
|
class QuoteCard extends StatefulWidget {
|
||||||
|
const QuoteCard({
|
||||||
|
super.key,
|
||||||
|
required this.quote,
|
||||||
|
required this.meta,
|
||||||
|
required this.onVoteUp,
|
||||||
|
required this.onVoteDown,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Quote quote;
|
||||||
|
final String meta;
|
||||||
|
final VoidCallback onVoteUp;
|
||||||
|
final VoidCallback onVoteDown;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<QuoteCard> createState() => _QuoteCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuoteCardState extends State<QuoteCard> {
|
||||||
|
static const int _collapsedMaxLines = 4;
|
||||||
|
static const int _lengthHintForMore = 160;
|
||||||
|
|
||||||
|
bool _expanded = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = Theme.of(context);
|
||||||
|
final baseSmall = t.textTheme.bodySmall;
|
||||||
|
final baseSmallColor = baseSmall?.color;
|
||||||
|
final metaColor = baseSmallColor?.withValues(alpha: 0.70);
|
||||||
|
|
||||||
|
final showMoreToggle = widget.quote.text.length > _lengthHintForMore;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 1,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.quote.text,
|
||||||
|
style: t.textTheme.bodyLarge,
|
||||||
|
softWrap: true,
|
||||||
|
maxLines: _expanded ? null : _collapsedMaxLines,
|
||||||
|
overflow: _expanded
|
||||||
|
? TextOverflow.visible
|
||||||
|
: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.meta,
|
||||||
|
style: baseSmall?.copyWith(color: metaColor),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showMoreToggle) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () =>
|
||||||
|
setState(() => _expanded = !_expanded),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
minimumSize: const Size(0, 0),
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
child: Text(_expanded ? 'Less' : 'More'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints.tightFor(width: 56),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// RailDivider(),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
//todo(timo) michi hauepl icon
|
||||||
|
VoteButton(
|
||||||
|
icon: Icons.arrow_upward,
|
||||||
|
semantics: 'Upvote',
|
||||||
|
count: widget.quote.upvotes,
|
||||||
|
onPressed: widget.onVoteUp,
|
||||||
|
color: t.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
VoteButton(
|
||||||
|
icon: Icons.arrow_downward,
|
||||||
|
semantics: 'Downvote',
|
||||||
|
count: widget.quote.downvotes,
|
||||||
|
onPressed: widget.onVoteDown,
|
||||||
|
color: t.colorScheme.error,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
wien_talks/wien_talks_flutter/lib/widgets/rail_divider.dart
Normal file
17
wien_talks/wien_talks_flutter/lib/widgets/rail_divider.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RailDivider extends StatelessWidget {
|
||||||
|
const RailDivider({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final c = Theme.of(context).dividerColor.withValues(alpha: 0.40);
|
||||||
|
return Container(
|
||||||
|
height: 18,
|
||||||
|
width: 1,
|
||||||
|
margin: const EdgeInsets.only(bottom: 6),
|
||||||
|
color: c,
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
wien_talks/wien_talks_flutter/lib/widgets/vote_button.dart
Normal file
50
wien_talks/wien_talks_flutter/lib/widgets/vote_button.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:wien_talks_client/wien_talks_client.dart';
|
||||||
|
|
||||||
|
class VoteButton extends StatelessWidget {
|
||||||
|
const VoteButton({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.semantics,
|
||||||
|
required this.count,
|
||||||
|
required this.onPressed,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String semantics;
|
||||||
|
final int count;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = Theme.of(context);
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
icon: Icon(icon),
|
||||||
|
tooltip: semantics,
|
||||||
|
color: color,
|
||||||
|
iconSize: 20,
|
||||||
|
constraints: const BoxConstraints.tightFor(width: 36, height: 36),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
splashRadius: 20,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
transitionBuilder: (child, anim) =>
|
||||||
|
ScaleTransition(scale: anim, child: child),
|
||||||
|
child: Text(
|
||||||
|
'$count',
|
||||||
|
key: ValueKey(count),
|
||||||
|
style: t.textTheme.labelSmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,14 +23,13 @@ DEPLOY_NETWORK = docker-net
|
||||||
.PHONY: local local-env local-stop local-down local-clean
|
.PHONY: local local-env local-stop local-down local-clean
|
||||||
local: .env
|
local: .env
|
||||||
docker compose -f $(COMPOSE_FILE_LOCAL) up -d
|
docker compose -f $(COMPOSE_FILE_LOCAL) up -d
|
||||||
|
|
||||||
local-env: .env
|
local-env: .env
|
||||||
|
|
||||||
local-stop:
|
local-stop:
|
||||||
docker compose -f $(COMPOSE_FILE_LOCAL) stop
|
docker compose -f $(COMPOSE_FILE_LOCAL) stop
|
||||||
|
|
||||||
local-down:
|
local-down:
|
||||||
docker compose -f $(COMPOSE_FILE_LOCAL) down
|
docker compose -f $(COMPOSE_FILE_LOCAL) down -v
|
||||||
|
|
||||||
local-clean: local-down
|
local-clean: local-down
|
||||||
for VOLUME in $(shell docker compose -f $(COMPOSE_FILE_LOCAL) volumes -q); \
|
for VOLUME in $(shell docker compose -f $(COMPOSE_FILE_LOCAL) volumes -q); \
|
||||||
|
|
@ -66,3 +65,12 @@ deploy-clean: deploy-down
|
||||||
if test -n "$$(docker network ls -q --filter name=$(DEPLOY_NETWORK))"; then \
|
if test -n "$$(docker network ls -q --filter name=$(DEPLOY_NETWORK))"; then \
|
||||||
docker network rm $(DEPLOY_NETWORK) > /dev/null; fi
|
docker network rm $(DEPLOY_NETWORK) > /dev/null; fi
|
||||||
|
|
||||||
|
codegen:
|
||||||
|
rm -rf lib/src/generated
|
||||||
|
serverpod generate
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
dart run bin/main.dart --role maintenance --apply-migrations
|
||||||
|
|
||||||
|
recreate-db: local-down local codegen migrate
|
||||||
|
@echo "DB recreated & migrations applied."
|
||||||
Loading…
Add table
Reference in a new issue