diff --git a/wien_talks/wien_talks_flutter/lib/helper/funmap_mgr.dart b/wien_talks/wien_talks_flutter/lib/helper/funmap_mgr.dart index e213c4e..426d5f7 100644 --- a/wien_talks/wien_talks_flutter/lib/helper/funmap_mgr.dart +++ b/wien_talks/wien_talks_flutter/lib/helper/funmap_mgr.dart @@ -10,7 +10,7 @@ class FunmapMgr { late Client client; - late final serverUrl; + late final String serverUrl; factory FunmapMgr() { if (_instance != null) return _instance!; @@ -26,9 +26,11 @@ class FunmapMgr { // E.g. `flutter run --dart-define=SERVER_URL=https://api.example.com/` 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(); } diff --git a/wien_talks/wien_talks_flutter/lib/helper/go_router.dart b/wien_talks/wien_talks_flutter/lib/helper/go_router.dart index 2c3c38b..679cd7e 100644 --- a/wien_talks/wien_talks_flutter/lib/helper/go_router.dart +++ b/wien_talks/wien_talks_flutter/lib/helper/go_router.dart @@ -1,19 +1,9 @@ -import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:wien_talks_flutter/create_event_screen.dart'; -import 'package:wien_talks_flutter/helper/auth_service.dart'; -import 'package:wien_talks_flutter/login_page.dart'; -import 'package:wien_talks_flutter/news_screen.dart'; +import 'package:wien_talks_flutter/screens/create_event_screen.dart'; +import 'package:wien_talks_flutter/screens/login_page.dart'; +import 'package:wien_talks_flutter/screens/news_screen.dart'; 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: [ GoRoute(path: '/login', builder: (c, s) => const LoginScreen()), GoRoute(path: '/', builder: (c, s) => NewsScreen()), @@ -23,9 +13,3 @@ final router = GoRouter( builder: (c, s) => CreateEventScreen()), ], ); - -class AuthChangeNotifier extends ChangeNotifier { - AuthChangeNotifier() { - AuthService.onUserChanged.listen((_) => notifyListeners()); - } -} diff --git a/wien_talks/wien_talks_flutter/lib/location_mgr.dart b/wien_talks/wien_talks_flutter/lib/helper/location_mgr.dart similarity index 80% rename from wien_talks/wien_talks_flutter/lib/location_mgr.dart rename to wien_talks/wien_talks_flutter/lib/helper/location_mgr.dart index 0893c5c..3dbcfb3 100644 --- a/wien_talks/wien_talks_flutter/lib/location_mgr.dart +++ b/wien_talks/wien_talks_flutter/lib/helper/location_mgr.dart @@ -35,7 +35,8 @@ class LocationMgr { final SymbolCache symbolCache = FileSymbolCache(); - final JobRenderer jobRenderer = kIsWeb ? MapOnlineRendererWeb() : MapOnlineRenderer(); + final JobRenderer jobRenderer = + kIsWeb ? MapOnlineRendererWeb() : MapOnlineRenderer(); final MarkerByItemDataStore markerDataStore = MarkerByItemDataStore(); @@ -44,7 +45,7 @@ class LocationMgr { return _instance!; } - LocationMgr._() {} + LocationMgr._(); Future startup() async { serviceEnabled = await location.serviceEnabled(); @@ -70,18 +71,23 @@ class LocationMgr { ); mapModel?.markerDataStores.add(markerDataStore); viewModel = ViewModel(displayModel: displayModel); - _subscription = location.onLocationChanged.listen((LocationData currentLocation) { + _subscription = + location.onLocationChanged.listen((LocationData currentLocation) { _lastLocationData = currentLocation; - if (currentLocation.latitude != null && currentLocation.longitude != null) { - viewModel?.setMapViewPosition(currentLocation.latitude!, currentLocation.longitude!); + if (currentLocation.latitude != null && + currentLocation.longitude != null) { + viewModel?.setMapViewPosition( + currentLocation.latitude!, currentLocation.longitude!); if (iconMarker == null) { iconMarker ??= IconMarker( fontSize: 30, icon: Icons.gps_fixed, color: Colors.red, - center: LatLong(currentLocation.latitude!, currentLocation.longitude!), + center: LatLong( + currentLocation.latitude!, currentLocation.longitude!), displayModel: displayModel); - mapModel?.markerDataStores.add(MarkerDataStore()..addMarker(iconMarker!)); + mapModel?.markerDataStores + .add(MarkerDataStore()..addMarker(iconMarker!)); } } _subject.add(currentLocation); diff --git a/wien_talks/wien_talks_flutter/lib/mapfile_widget.dart b/wien_talks/wien_talks_flutter/lib/mapfile_widget.dart index 477e2a5..c8c1958 100644 --- a/wien_talks/wien_talks_flutter/lib/mapfile_widget.dart +++ b/wien_talks/wien_talks_flutter/lib/mapfile_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/cupertino.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 { const MapfileWidget({super.key}); diff --git a/wien_talks/wien_talks_flutter/lib/create_event_screen.dart b/wien_talks/wien_talks_flutter/lib/screens/create_event_screen.dart similarity index 68% rename from wien_talks/wien_talks_flutter/lib/create_event_screen.dart rename to wien_talks/wien_talks_flutter/lib/screens/create_event_screen.dart index 8aa88ab..d31166f 100644 --- a/wien_talks/wien_talks_flutter/lib/create_event_screen.dart +++ b/wien_talks/wien_talks_flutter/lib/screens/create_event_screen.dart @@ -1,13 +1,13 @@ import 'package:flutter/cupertino.dart'; import 'package:location/location.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/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 'location_mgr.dart'; +import '../helper/location_mgr.dart'; class CreateEventScreen extends StatelessWidget { const CreateEventScreen({super.key}); @@ -24,7 +24,11 @@ class CreateEventScreen extends StatelessWidget { ), StreamBuilder( stream: LocationMgr().stream, - builder: (BuildContext context, AsyncSnapshot snapshot) => snapshot.data != null ? Text(snapshot.data.toString()) : SizedBox()), + builder: + (BuildContext context, AsyncSnapshot snapshot) => + snapshot.data != null + ? Text(snapshot.data.toString()) + : SizedBox()), Expanded( child: GetLocationWidget( child: MapfileWidget(), diff --git a/wien_talks/wien_talks_flutter/lib/home_screen.dart b/wien_talks/wien_talks_flutter/lib/screens/home_screen.dart similarity index 80% rename from wien_talks/wien_talks_flutter/lib/home_screen.dart rename to wien_talks/wien_talks_flutter/lib/screens/home_screen.dart index ac051f4..e5de7a2 100644 --- a/wien_talks/wien_talks_flutter/lib/home_screen.dart +++ b/wien_talks/wien_talks_flutter/lib/screens/home_screen.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.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/screen_widget.dart'; -import 'carousel_widget.dart'; +import '../widgets/carousel_widget.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({ @@ -29,8 +29,10 @@ class HomeScreen extends StatelessWidget { Expanded( child: ElevatedButton( style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(Theme.of(context).primaryColor), - foregroundColor: WidgetStateProperty.all(Theme.of(context).colorScheme.onPrimary)), + backgroundColor: WidgetStateProperty.all( + Theme.of(context).primaryColor), + foregroundColor: WidgetStateProperty.all( + Theme.of(context).colorScheme.onPrimary)), onPressed: () { context.pushNamed("create_event"); }, @@ -45,7 +47,8 @@ class HomeScreen extends StatelessWidget { Row( children: [ Spacer(), - Text(FunmapMgr().serverUrl, style: Theme.of(context).textTheme.bodySmall), + Text(FunmapMgr().serverUrl, + style: Theme.of(context).textTheme.bodySmall), ], ) ], diff --git a/wien_talks/wien_talks_flutter/lib/login_page.dart b/wien_talks/wien_talks_flutter/lib/screens/login_page.dart similarity index 100% rename from wien_talks/wien_talks_flutter/lib/login_page.dart rename to wien_talks/wien_talks_flutter/lib/screens/login_page.dart diff --git a/wien_talks/wien_talks_flutter/lib/news_screen.dart b/wien_talks/wien_talks_flutter/lib/screens/news_screen.dart similarity index 86% rename from wien_talks/wien_talks_flutter/lib/news_screen.dart rename to wien_talks/wien_talks_flutter/lib/screens/news_screen.dart index 84507cb..3701beb 100644 --- a/wien_talks/wien_talks_flutter/lib/news_screen.dart +++ b/wien_talks/wien_talks_flutter/lib/screens/news_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.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/screen_widget.dart'; @@ -16,7 +16,7 @@ class NewsScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - HeadingText(text: "Latest news"), + HeadingText(text: "What's being said"), ShowLatestNewsWidget(), SizedBox( height: 30, diff --git a/wien_talks/wien_talks_flutter/lib/screens/show_latest_news_widget.dart b/wien_talks/wien_talks_flutter/lib/screens/show_latest_news_widget.dart new file mode 100644 index 0000000..14fbb22 --- /dev/null +++ b/wien_talks/wien_talks_flutter/lib/screens/show_latest_news_widget.dart @@ -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 createState() => _ShowLatestNewsWidgetState(); +} + +class _ShowLatestNewsWidgetState extends State { + List? _quotes; + Object? _error; + bool _loading = true; + + @override + void initState() { + super.initState(); + _fetch(); + } + + Future _fetch() async { + setState(() { + _loading = true; + _error = null; + }); + try { + final list = await FunmapMgr().client.quote.getAllQuotes(); + final quotes = list.whereType().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 _refresh() => _fetch(); + + Future _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.from(_quotes!); + copy[idx] = updated; + _quotes = copy; + }); + + try { + await FunmapMgr().client.quote.updateQuote(updated); + } catch (e) { + setState(() { + final copy = List.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 []; + 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); + }, + ); + } +} diff --git a/wien_talks/wien_talks_flutter/lib/show_latest_news_widget.dart b/wien_talks/wien_talks_flutter/lib/show_latest_news_widget.dart deleted file mode 100644 index 9938649..0000000 --- a/wien_talks/wien_talks_flutter/lib/show_latest_news_widget.dart +++ /dev/null @@ -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> _load() async { - final list = await FunmapMgr().client.quote.getAllQuotes(); - return list.whereType().toList(growable: false); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder>( - 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 []; - 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(' · ')), - ); - }, - ); - }, - ); - } -} diff --git a/wien_talks/wien_talks_flutter/lib/carousel_widget.dart b/wien_talks/wien_talks_flutter/lib/widgets/carousel_widget.dart similarity index 100% rename from wien_talks/wien_talks_flutter/lib/carousel_widget.dart rename to wien_talks/wien_talks_flutter/lib/widgets/carousel_widget.dart diff --git a/wien_talks/wien_talks_flutter/lib/get_location_widget.dart b/wien_talks/wien_talks_flutter/lib/widgets/get_location_widget.dart similarity index 86% rename from wien_talks/wien_talks_flutter/lib/get_location_widget.dart rename to wien_talks/wien_talks_flutter/lib/widgets/get_location_widget.dart index 9fe8008..c7d738d 100644 --- a/wien_talks/wien_talks_flutter/lib/get_location_widget.dart +++ b/wien_talks/wien_talks_flutter/lib/widgets/get_location_widget.dart @@ -1,5 +1,5 @@ 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 { final Widget child; @@ -31,7 +31,8 @@ class _GetLocationWidgetState extends State { { if (snapshot.hasData) { // Error occured - return Text(snapshot.data.toString(), style: TextStyle(color: Colors.red)); + return Text(snapshot.data.toString(), + style: TextStyle(color: Colors.red)); } else { return widget.child; } diff --git a/wien_talks/wien_talks_flutter/lib/news_input_form.dart b/wien_talks/wien_talks_flutter/lib/widgets/news_input_form.dart similarity index 83% rename from wien_talks/wien_talks_flutter/lib/news_input_form.dart rename to wien_talks/wien_talks_flutter/lib/widgets/news_input_form.dart index 3e63217..9c9a5f0 100644 --- a/wien_talks/wien_talks_flutter/lib/news_input_form.dart +++ b/wien_talks/wien_talks_flutter/lib/widgets/news_input_form.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:loader_overlay/loader_overlay.dart'; import 'package:location/location.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'; typedef OnSubmit = Future Function(CreateQuoteRequest request); @@ -28,8 +28,11 @@ class _NewsInputFormState extends State { void _submitForm() async { LocationData? locationData = LocationMgr().lastLocation; - if (locationData == null || locationData.latitude == null || locationData.longitude == null) { - ErrorSnackbar().show(context, "No location available, please retry later"); + if (locationData == null || + locationData.latitude == null || + locationData.longitude == null) { + ErrorSnackbar() + .show(context, "No location available, please retry later"); return; } if (_formKey.currentState!.validate()) { @@ -79,8 +82,10 @@ class _NewsInputFormState extends State { const SizedBox(height: 16.0), ElevatedButton( style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(Theme.of(context).primaryColor), - foregroundColor: WidgetStateProperty.all(Theme.of(context).colorScheme.onPrimary)), + backgroundColor: + WidgetStateProperty.all(Theme.of(context).primaryColor), + foregroundColor: WidgetStateProperty.all( + Theme.of(context).colorScheme.onPrimary)), onPressed: _submitForm, child: const Text('Submit News'), ), diff --git a/wien_talks/wien_talks_flutter/lib/widgets/quote_card.dart b/wien_talks/wien_talks_flutter/lib/widgets/quote_card.dart new file mode 100644 index 0000000..65f3832 --- /dev/null +++ b/wien_talks/wien_talks_flutter/lib/widgets/quote_card.dart @@ -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 createState() => _QuoteCardState(); +} + +class _QuoteCardState extends State { + 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, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/wien_talks/wien_talks_flutter/lib/widgets/rail_divider.dart b/wien_talks/wien_talks_flutter/lib/widgets/rail_divider.dart new file mode 100644 index 0000000..07a20f5 --- /dev/null +++ b/wien_talks/wien_talks_flutter/lib/widgets/rail_divider.dart @@ -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, + ); + } +} diff --git a/wien_talks/wien_talks_flutter/lib/widgets/vote_button.dart b/wien_talks/wien_talks_flutter/lib/widgets/vote_button.dart new file mode 100644 index 0000000..92c242f --- /dev/null +++ b/wien_talks/wien_talks_flutter/lib/widgets/vote_button.dart @@ -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, + ), + ), + ], + ); + } +} diff --git a/wien_talks/wien_talks_server/Makefile b/wien_talks/wien_talks_server/Makefile index ab420e8..194a388 100644 --- a/wien_talks/wien_talks_server/Makefile +++ b/wien_talks/wien_talks_server/Makefile @@ -23,14 +23,13 @@ DEPLOY_NETWORK = docker-net .PHONY: local local-env local-stop local-down local-clean local: .env docker compose -f $(COMPOSE_FILE_LOCAL) up -d - local-env: .env local-stop: docker compose -f $(COMPOSE_FILE_LOCAL) stop local-down: - docker compose -f $(COMPOSE_FILE_LOCAL) down + docker compose -f $(COMPOSE_FILE_LOCAL) down -v local-clean: local-down 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 \ 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." \ No newline at end of file