Compare commits

..

No commits in common. "1d066e1e1177260f1c38063c7a0f3961a2181e7c" and "2b84f749aea3681eac1a20d29da87c6d52accdda" have entirely different histories.

9 changed files with 135 additions and 383 deletions

View file

@ -26,9 +26,8 @@ class FunmapMgr {
// E.g. `flutter run --dart-define=SERVER_URL=https://api.example.com/`
const serverUrlFromEnv = String.fromEnvironment('SERVER_URL');
serverUrl = serverUrlFromEnv.isEmpty
? 'https://wien-talks-api.aequito.sh/'
: serverUrlFromEnv;
serverUrl =
serverUrlFromEnv.isEmpty ? 'http://$localhost:8080/' : serverUrlFromEnv;
client = Client(serverUrl, connectionTimeout: const Duration(seconds: 2))
..connectivityMonitor = FlutterConnectivityMonitor();

View file

@ -1,51 +1,15 @@
import 'package:flutter/material.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/login_page.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(
initialLocation: '/',
routes: [
GoRoute(path: '/login', builder: (c, s) => const LoginScreen()),
GoRoute(path: '/', builder: (c, s) => NewsScreen()),
GoRoute(
path: '/',
name: 'news',
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,
),
);
},
);
},
),
path: '/create_event',
name: 'create_event',
builder: (c, s) => CreateEventScreen()),
],
);

View file

@ -1,195 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:location/location.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/carousel_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/widgets/mapfile_widget.dart';
import 'package:wien_talks_flutter/widgets/news_input_form.dart';
import 'package:wien_talks_flutter/widgets/screen_widget.dart';
class CreateEventScreen extends StatefulWidget {
const CreateEventScreen({
super.key,
this.contextQuote,
this.initialLat,
this.initialLon,
});
import '../helper/location_mgr.dart';
final Quote? contextQuote;
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);
}
}
class CreateEventScreen extends StatelessWidget {
const CreateEventScreen({super.key});
@override
Widget build(BuildContext context) {
final hasContextQuote = widget.contextQuote != null;
final quote = widget.contextQuote;
return ScreenWidget(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (hasContextQuote) ...[
Card(
margin: const EdgeInsets.fromLTRB(12, 12, 12, 6),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(quote!.text,
style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 6),
Text(
[
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'),
),
],
),
child: Column(
children: [
NewsInputForm(
onSubmit: (CreateQuoteRequest request) async {
await FunmapMgr().client.quote.createQuote(request);
},
),
StreamBuilder(
stream: LocationMgr().stream,
builder:
(BuildContext context, AsyncSnapshot<LocationData> snapshot) =>
snapshot.data != null
? Text(snapshot.data.toString())
: SizedBox()),
Expanded(
child: GetLocationWidget(
child: MapfileWidget(),
),
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;
}),
),
),
],
),
),
],
),
);
),
],
));
}
}

View file

@ -5,6 +5,8 @@ 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/screen_widget.dart';
import '../widgets/carousel_widget.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({
super.key,
@ -41,6 +43,7 @@ class HomeScreen extends StatelessWidget {
SizedBox(
height: 30,
),
CarouselWidget(),
Row(
children: [
Spacer(),

View file

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:flutter/material.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_flutter/helper/funmap_mgr.dart';
import 'package:wien_talks_flutter/helper/location_filter.dart';
@ -213,14 +212,6 @@ class _LatestQuotesScreenState extends State<LatestQuotesScreen> {
_applyFilters();
},
staticMapUrlBuilder: gStaticMap,
onTap: () => context.pushNamed(
'create_event',
extra: q,
queryParameters: {
'lat': q.lat.toString(),
'lon': q.long.toString(),
},
),
);
},
)),

View file

@ -0,0 +1,50 @@
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'),
),
],
),
),
);
}
}

View file

@ -1,110 +1,25 @@
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
class LocationSuggestion {
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;
class CarouselWidget extends StatelessWidget {
const CarouselWidget({super.key});
@override
Widget build(BuildContext context) {
if (suggestions.isEmpty) return const SizedBox.shrink();
return SafeArea(
top: false,
child: Card(
margin: const EdgeInsets.fromLTRB(12, 0, 12, 12),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: CarouselSlider(
options: CarouselOptions(
height: height,
viewportFraction: 0.72,
enlargeCenterPage: true,
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(),
),
),
return IgnorePointer(
child: CarouselSlider(
options: CarouselOptions(height: 300.0, autoPlay: true),
items: ["houses.jpg", "kangaroos.jpg", "sightseeing.jpg", "tram.jpg", "fiaker.jpg", "falco.jpg", "wastebin.jpg"].map((i) {
return Builder(
builder: (BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width,
margin: EdgeInsets.symmetric(horizontal: 5.0),
//decoration: BoxDecoration(color: Colors.amber),
child: Image(image: AssetImage("assets/funny_images/$i")));
},
);
}).toList(),
),
);
}

View file

@ -37,7 +37,7 @@ class FilterChipsBar extends StatelessWidget {
),
const SizedBox(width: 8),
ChoiceChip(
label: const Text('Loved'),
label: const Text('Loveed'),
selected: sort == 'top',
onSelected: (_) => onSortChanged('top'),
),

View file

@ -21,7 +21,6 @@ class FlamboyantQuoteCard extends StatelessWidget {
required this.onVoteUp,
required this.onVoteDown,
this.staticMapUrlBuilder,
this.onTap,
});
final Quote quote;
@ -29,20 +28,21 @@ class FlamboyantQuoteCard extends StatelessWidget {
final VoidCallback onVoteUp;
final VoidCallback onVoteDown;
final StaticMapUrlBuilder? staticMapUrlBuilder;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final seed = (quote.id ?? quote.text.hashCode) & 0x7fffffff;
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 tiltRad = tiltDeg * math.pi / 180.0;
final accents = [
const Color(0xFFE53935),
const Color(0xFF3949AB),
const Color(0xFF00897B),
const Color(0xFFE53935), // red
const Color(0xFF3949AB), // indigo
const Color(0xFF00897B), // teal
];
final accent = accents[seed % accents.length];
@ -52,41 +52,29 @@ class FlamboyantQuoteCard extends StatelessWidget {
.withValues(alpha: 0.70),
);
final borderRadius = BorderRadius.circular(14);
final cardContent = CardContenty(
quote: quote,
staticMapUrlBuilder: staticMapUrlBuilder,
meta: meta,
onVoteUp: onVoteUp,
onVoteDown: onVoteDown,
context: context,
variant: variant,
accent: accent,
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,
),
final card = Container(
decoration: BoxDecoration(
color: t.colorScheme.surface,
borderRadius: BorderRadius.circular(14),
boxShadow: const [
BoxShadow(
color: Color(0x14000000),
blurRadius: 12,
offset: Offset(0, 6),
),
],
border: Border.all(color: accent.withValues(alpha: 0.25), width: 1),
),
child: CardContenty(
quote: quote,
staticMapUrlBuilder: staticMapUrlBuilder,
meta: meta,
onVoteUp: onVoteUp,
onVoteDown: onVoteDown,
context: context,
variant: variant,
accent: accent,
metaStyle: metaStyle),
);
return Padding(
@ -96,17 +84,14 @@ class FlamboyantQuoteCard extends StatelessWidget {
children: [
Transform.rotate(
angle: tiltRad,
transformHitTests: false,
child: tappableCard,
child: card,
),
Positioned(
top: -8,
right: 16,
child: IgnorePointer(
child: UbahnTape(
lat: quote.lat,
lon: quote.long,
),
child: UbahnTape(
lat: quote.lat,
lon: quote.long,
),
),
],