restyle quote list

This commit is contained in:
tk 2025-08-17 01:07:32 +02:00
parent 595b4e730e
commit 39e4f1142f
17 changed files with 394 additions and 107 deletions

View file

@ -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();
} }

View file

@ -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());
}
}

View file

@ -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);

View file

@ -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});

View file

@ -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(),

View file

@ -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),
], ],
) )
], ],

View file

@ -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,

View file

@ -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);
},
);
}
}

View file

@ -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(' · ')),
);
},
);
},
);
}
}

View file

@ -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;
} }

View file

@ -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'),
), ),

View 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,
),
],
),
),
],
),
),
);
}
}

View 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,
);
}
}

View 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,
),
),
],
);
}
}

View file

@ -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."