mirror of
https://github.com/timokz/flutter-vienna-hackathon-25.git
synced 2025-11-08 19:04:20 +01:00
reintroduce carousel and local map
This commit is contained in:
parent
da5fe81b1b
commit
1d066e1e11
8 changed files with 382 additions and 134 deletions
|
|
@ -26,8 +26,9 @@ 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 =
|
serverUrl = serverUrlFromEnv.isEmpty
|
||||||
serverUrlFromEnv.isEmpty ? 'http://$localhost:8080/' : serverUrlFromEnv;
|
? 'https://wien-talks-api.aequito.sh/'
|
||||||
|
: serverUrlFromEnv;
|
||||||
|
|
||||||
client = Client(serverUrl, connectionTimeout: const Duration(seconds: 2))
|
client = Client(serverUrl, connectionTimeout: const Duration(seconds: 2))
|
||||||
..connectivityMonitor = FlutterConnectivityMonitor();
|
..connectivityMonitor = FlutterConnectivityMonitor();
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,51 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:wien_talks_client/wien_talks_client.dart';
|
||||||
import 'package:wien_talks_flutter/screens/create_event_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';
|
import 'package:wien_talks_flutter/screens/news_screen.dart';
|
||||||
|
|
||||||
|
double? _qpDouble(GoRouterState s, String key) {
|
||||||
|
final v = s.uri.queryParameters[key];
|
||||||
|
return v == null || v.isEmpty ? null : double.tryParse(v);
|
||||||
|
}
|
||||||
|
|
||||||
final router = GoRouter(
|
final router = GoRouter(
|
||||||
|
initialLocation: '/',
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(path: '/login', builder: (c, s) => const LoginScreen()),
|
|
||||||
GoRoute(path: '/', builder: (c, s) => NewsScreen()),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/create_event',
|
path: '/',
|
||||||
name: 'create_event',
|
name: 'news',
|
||||||
builder: (c, s) => CreateEventScreen()),
|
builder: (c, s) => NewsScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/create_event',
|
||||||
|
name: 'create_event',
|
||||||
|
pageBuilder: (c, s) {
|
||||||
|
final quote = s.extra as Quote?;
|
||||||
|
final lat = _qpDouble(s, 'lat');
|
||||||
|
final lon = _qpDouble(s, 'lon');
|
||||||
|
|
||||||
|
return CustomTransitionPage(
|
||||||
|
key: s.pageKey,
|
||||||
|
child: CreateEventScreen(
|
||||||
|
contextQuote: quote,
|
||||||
|
initialLat: lat,
|
||||||
|
initialLon: lon,
|
||||||
|
),
|
||||||
|
transitionsBuilder: (context, anim, secAnim, child) {
|
||||||
|
final curve =
|
||||||
|
CurvedAnimation(parent: anim, curve: Curves.easeOutCubic);
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: curve,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: Tween(begin: const Offset(0, 0.08), end: Offset.zero)
|
||||||
|
.animate(curve),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,195 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/material.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/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/widgets/carousel_widget.dart';
|
||||||
|
import 'package:wien_talks_flutter/widgets/get_location_widget.dart';
|
||||||
import 'package:wien_talks_flutter/widgets/mapfile_widget.dart';
|
import 'package:wien_talks_flutter/widgets/mapfile_widget.dart';
|
||||||
import 'package:wien_talks_flutter/widgets/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 '../helper/location_mgr.dart';
|
class CreateEventScreen extends StatefulWidget {
|
||||||
|
const CreateEventScreen({
|
||||||
|
super.key,
|
||||||
|
this.contextQuote,
|
||||||
|
this.initialLat,
|
||||||
|
this.initialLon,
|
||||||
|
});
|
||||||
|
|
||||||
class CreateEventScreen extends StatelessWidget {
|
final Quote? contextQuote;
|
||||||
const CreateEventScreen({super.key});
|
final double? initialLat;
|
||||||
|
final double? initialLon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CreateEventScreen> createState() => _CreateEventScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateEventScreenState extends State<CreateEventScreen> {
|
||||||
|
double? _lat;
|
||||||
|
double? _lon;
|
||||||
|
bool _submitting = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_lat = widget.contextQuote?.lat ?? widget.initialLat;
|
||||||
|
_lon = widget.contextQuote?.long ?? widget.initialLon;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _useMyLocation() async {
|
||||||
|
try {
|
||||||
|
final loc = await Location().getLocation();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_lat = loc.latitude;
|
||||||
|
_lon = loc.longitude;
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Couldn`’t get your location.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit(CreateQuoteRequest req) async {
|
||||||
|
final lat = _lat ?? widget.contextQuote?.lat;
|
||||||
|
final lon = _lon ?? widget.contextQuote?.long;
|
||||||
|
|
||||||
|
if (lat == null || lon == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text(' location first.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final toSend = CreateQuoteRequest(
|
||||||
|
text: req.text,
|
||||||
|
authorName: req.authorName,
|
||||||
|
lat: lat,
|
||||||
|
lng: lon,
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() => _submitting = true);
|
||||||
|
try {
|
||||||
|
await FunmapMgr().client.quote.createQuote(toSend);
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.pop(context);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Posting failed: $e')),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _submitting = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final hasContextQuote = widget.contextQuote != null;
|
||||||
|
final quote = widget.contextQuote;
|
||||||
|
|
||||||
return ScreenWidget(
|
return ScreenWidget(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
NewsInputForm(
|
children: [
|
||||||
onSubmit: (CreateQuoteRequest request) async {
|
if (hasContextQuote) ...[
|
||||||
await FunmapMgr().client.quote.createQuote(request);
|
Card(
|
||||||
},
|
margin: const EdgeInsets.fromLTRB(12, 12, 12, 6),
|
||||||
),
|
child: Padding(
|
||||||
StreamBuilder(
|
padding: const EdgeInsets.all(12),
|
||||||
stream: LocationMgr().stream,
|
child: Column(
|
||||||
builder:
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
(BuildContext context, AsyncSnapshot<LocationData> snapshot) =>
|
children: [
|
||||||
snapshot.data != null
|
Text(quote!.text,
|
||||||
? Text(snapshot.data.toString())
|
style: Theme.of(context).textTheme.bodyLarge),
|
||||||
: SizedBox()),
|
const SizedBox(height: 6),
|
||||||
Expanded(
|
Text(
|
||||||
child: GetLocationWidget(
|
[
|
||||||
child: MapfileWidget(),
|
if ((quote.authorName ?? '').trim().isNotEmpty)
|
||||||
|
quote.authorName!.trim()
|
||||||
|
].join(' · '),
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_lat != null && _lon != null
|
||||||
|
? 'Location: ${_lat!.toStringAsFixed(5)}, ${_lon!.toStringAsFixed(5)}'
|
||||||
|
: 'Pick an alt location',
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: _useMyLocation,
|
||||||
|
icon: const Icon(Icons.my_location, size: 18),
|
||||||
|
label: const Text('My location'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Padding(
|
||||||
],
|
padding: const EdgeInsets.fromLTRB(12, 4, 12, 8),
|
||||||
));
|
child: NewsInputForm(
|
||||||
|
// enabled: !_submitting,
|
||||||
|
onSubmit: _submit,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
GetLocationWidget(
|
||||||
|
child: MapfileWidget(),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: LocationCarousel(
|
||||||
|
suggestions: [
|
||||||
|
if (widget.contextQuote?.lat != null &&
|
||||||
|
widget.contextQuote?.long != null)
|
||||||
|
LocationSuggestion(
|
||||||
|
label: 'Quote location',
|
||||||
|
lat: widget.contextQuote!.lat,
|
||||||
|
lon: widget.contextQuote!.long,
|
||||||
|
assetImage: 'assets/funny_images/tram.jpg',
|
||||||
|
),
|
||||||
|
const LocationSuggestion(
|
||||||
|
label: 'Stephansplatz',
|
||||||
|
lat: 48.2085,
|
||||||
|
lon: 16.3731,
|
||||||
|
assetImage: 'assets/funny_images/sightseeing.jpg'),
|
||||||
|
const LocationSuggestion(
|
||||||
|
label: 'Naschmarkt',
|
||||||
|
lat: 48.1970,
|
||||||
|
lon: 16.3615,
|
||||||
|
assetImage: 'assets/funny_images/houses.jpg'),
|
||||||
|
],
|
||||||
|
onPick: (lat, lon) => setState(() {
|
||||||
|
_lat = lat;
|
||||||
|
_lon = lon;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import 'package:wien_talks_flutter/screens/latest_quotes_screen.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 '../widgets/carousel_widget.dart';
|
|
||||||
|
|
||||||
class HomeScreen extends StatelessWidget {
|
class HomeScreen extends StatelessWidget {
|
||||||
const HomeScreen({
|
const HomeScreen({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -43,7 +41,6 @@ class HomeScreen extends StatelessWidget {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 30,
|
height: 30,
|
||||||
),
|
),
|
||||||
CarouselWidget(),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Spacer(),
|
Spacer(),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
|
import 'package:go_router/go_router.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/location_filter.dart';
|
import 'package:wien_talks_flutter/helper/location_filter.dart';
|
||||||
|
|
@ -212,6 +213,14 @@ class _LatestQuotesScreenState extends State<LatestQuotesScreen> {
|
||||||
_applyFilters();
|
_applyFilters();
|
||||||
},
|
},
|
||||||
staticMapUrlBuilder: gStaticMap,
|
staticMapUrlBuilder: gStaticMap,
|
||||||
|
onTap: () => context.pushNamed(
|
||||||
|
'create_event',
|
||||||
|
extra: q,
|
||||||
|
queryParameters: {
|
||||||
|
'lat': q.lat.toString(),
|
||||||
|
'lon': q.long.toString(),
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:wien_talks_flutter/helper/auth_service.dart';
|
|
||||||
|
|
||||||
class LoginScreen extends StatelessWidget {
|
|
||||||
const LoginScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: Container(
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [Color(0xff2193b0), Color(0xff6dd5ed)],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('Wien Talks',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 42,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white)),
|
|
||||||
const SizedBox(height: 60),
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: () async => await AuthService.signIn(),
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
foregroundColor: Colors.black87,
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(30)),
|
|
||||||
elevation: 6,
|
|
||||||
),
|
|
||||||
icon: Icon(
|
|
||||||
Icons.lock,
|
|
||||||
),
|
|
||||||
label: const Text('Sign in with Google'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +1,110 @@
|
||||||
import 'package:carousel_slider/carousel_slider.dart';
|
import 'package:carousel_slider/carousel_slider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class CarouselWidget extends StatelessWidget {
|
class LocationSuggestion {
|
||||||
const CarouselWidget({super.key});
|
final String label;
|
||||||
|
final double lat;
|
||||||
|
final double lon;
|
||||||
|
final String? subtitle;
|
||||||
|
final String? assetImage;
|
||||||
|
const LocationSuggestion({
|
||||||
|
required this.label,
|
||||||
|
required this.lat,
|
||||||
|
required this.lon,
|
||||||
|
this.subtitle,
|
||||||
|
this.assetImage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocationCarousel extends StatelessWidget {
|
||||||
|
const LocationCarousel({
|
||||||
|
super.key,
|
||||||
|
required this.suggestions,
|
||||||
|
required this.onPick,
|
||||||
|
this.height = 96,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<LocationSuggestion> suggestions;
|
||||||
|
final void Function(double lat, double lon) onPick;
|
||||||
|
final double height;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IgnorePointer(
|
if (suggestions.isEmpty) return const SizedBox.shrink();
|
||||||
child: CarouselSlider(
|
|
||||||
options: CarouselOptions(height: 300.0, autoPlay: true),
|
return SafeArea(
|
||||||
items: ["houses.jpg", "kangaroos.jpg", "sightseeing.jpg", "tram.jpg", "fiaker.jpg", "falco.jpg", "wastebin.jpg"].map((i) {
|
top: false,
|
||||||
return Builder(
|
child: Card(
|
||||||
builder: (BuildContext context) {
|
margin: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||||
return Container(
|
child: Padding(
|
||||||
width: MediaQuery.of(context).size.width,
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
margin: EdgeInsets.symmetric(horizontal: 5.0),
|
child: CarouselSlider(
|
||||||
//decoration: BoxDecoration(color: Colors.amber),
|
options: CarouselOptions(
|
||||||
child: Image(image: AssetImage("assets/funny_images/$i")));
|
height: height,
|
||||||
},
|
viewportFraction: 0.72,
|
||||||
);
|
enlargeCenterPage: true,
|
||||||
}).toList(),
|
enableInfiniteScroll: suggestions.length > 1,
|
||||||
|
),
|
||||||
|
items: suggestions.map((s) {
|
||||||
|
return InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
onTap: () => onPick(s.lat, s.lon),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (s.assetImage != null)
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.horizontal(
|
||||||
|
left: Radius.circular(12)),
|
||||||
|
child: Image.asset(
|
||||||
|
s.assetImage!,
|
||||||
|
width: 90,
|
||||||
|
height: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12, vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(s.label,
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis),
|
||||||
|
if (s.subtitle != null)
|
||||||
|
Text(s.subtitle!,
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.labelSmall,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis),
|
||||||
|
Text(
|
||||||
|
'${s.lat.toStringAsFixed(4)}, ${s.lon.toStringAsFixed(4)}',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class FlamboyantQuoteCard extends StatelessWidget {
|
||||||
required this.onVoteUp,
|
required this.onVoteUp,
|
||||||
required this.onVoteDown,
|
required this.onVoteDown,
|
||||||
this.staticMapUrlBuilder,
|
this.staticMapUrlBuilder,
|
||||||
|
this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Quote quote;
|
final Quote quote;
|
||||||
|
|
@ -28,21 +29,20 @@ class FlamboyantQuoteCard extends StatelessWidget {
|
||||||
final VoidCallback onVoteUp;
|
final VoidCallback onVoteUp;
|
||||||
final VoidCallback onVoteDown;
|
final VoidCallback onVoteDown;
|
||||||
final StaticMapUrlBuilder? staticMapUrlBuilder;
|
final StaticMapUrlBuilder? staticMapUrlBuilder;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final seed = (quote.id ?? quote.text.hashCode) & 0x7fffffff;
|
final seed = (quote.id ?? quote.text.hashCode) & 0x7fffffff;
|
||||||
final rng = math.Random(seed);
|
final rng = math.Random(seed);
|
||||||
|
|
||||||
final variant = (rng.nextInt(3));
|
final variant = rng.nextInt(3);
|
||||||
|
|
||||||
// Subtle tilt and accent
|
|
||||||
final tiltDeg = [-2.2, -1.4, -0.6, 0, 0.6, 1.2, 2.0][rng.nextInt(7)];
|
final tiltDeg = [-2.2, -1.4, -0.6, 0, 0.6, 1.2, 2.0][rng.nextInt(7)];
|
||||||
final tiltRad = tiltDeg * math.pi / 180.0;
|
final tiltRad = tiltDeg * math.pi / 180.0;
|
||||||
final accents = [
|
final accents = [
|
||||||
const Color(0xFFE53935), // red
|
const Color(0xFFE53935),
|
||||||
const Color(0xFF3949AB), // indigo
|
const Color(0xFF3949AB),
|
||||||
const Color(0xFF00897B), // teal
|
const Color(0xFF00897B),
|
||||||
];
|
];
|
||||||
final accent = accents[seed % accents.length];
|
final accent = accents[seed % accents.length];
|
||||||
|
|
||||||
|
|
@ -52,29 +52,41 @@ class FlamboyantQuoteCard extends StatelessWidget {
|
||||||
.withValues(alpha: 0.70),
|
.withValues(alpha: 0.70),
|
||||||
);
|
);
|
||||||
|
|
||||||
final card = Container(
|
final borderRadius = BorderRadius.circular(14);
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: t.colorScheme.surface,
|
final cardContent = CardContenty(
|
||||||
borderRadius: BorderRadius.circular(14),
|
quote: quote,
|
||||||
boxShadow: const [
|
staticMapUrlBuilder: staticMapUrlBuilder,
|
||||||
BoxShadow(
|
meta: meta,
|
||||||
color: Color(0x14000000),
|
onVoteUp: onVoteUp,
|
||||||
blurRadius: 12,
|
onVoteDown: onVoteDown,
|
||||||
offset: Offset(0, 6),
|
context: context,
|
||||||
),
|
variant: variant,
|
||||||
],
|
accent: accent,
|
||||||
border: Border.all(color: accent.withValues(alpha: 0.25), width: 1),
|
metaStyle: metaStyle,
|
||||||
|
);
|
||||||
|
|
||||||
|
final tappableCard = Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: Ink(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: t.colorScheme.surface,
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Color(0x14000000),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: Offset(0, 6),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
border: Border.all(color: accent.withValues(alpha: 0.25), width: 1),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
onTap: onTap,
|
||||||
|
child: cardContent,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: CardContenty(
|
|
||||||
quote: quote,
|
|
||||||
staticMapUrlBuilder: staticMapUrlBuilder,
|
|
||||||
meta: meta,
|
|
||||||
onVoteUp: onVoteUp,
|
|
||||||
onVoteDown: onVoteDown,
|
|
||||||
context: context,
|
|
||||||
variant: variant,
|
|
||||||
accent: accent,
|
|
||||||
metaStyle: metaStyle),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
|
|
@ -84,14 +96,17 @@ class FlamboyantQuoteCard extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Transform.rotate(
|
Transform.rotate(
|
||||||
angle: tiltRad,
|
angle: tiltRad,
|
||||||
child: card,
|
transformHitTests: false,
|
||||||
|
child: tappableCard,
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: -8,
|
top: -8,
|
||||||
right: 16,
|
right: 16,
|
||||||
child: UbahnTape(
|
child: IgnorePointer(
|
||||||
lat: quote.lat,
|
child: UbahnTape(
|
||||||
lon: quote.long,
|
lat: quote.lat,
|
||||||
|
lon: quote.long,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue