mirror of
https://github.com/timokz/flutter-vienna-hackathon-25.git
synced 2025-11-08 23:44:19 +01:00
Compare commits
No commits in common. "eb5264a5533a4f4e2bbd8e2898ba02a1df6cd85a" and "47cfb949ac1c7d157531147e90da06ad67a9247d" have entirely different histories.
eb5264a553
...
47cfb949ac
12 changed files with 45 additions and 684 deletions
2
wien_talks/wien_talks_flutter/.gitignore
vendored
2
wien_talks/wien_talks_flutter/.gitignore
vendored
|
|
@ -45,5 +45,3 @@ app.*.map.json
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
|
||||||
.env
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
||||||
|
|
||||||
String gStaticMap(
|
|
||||||
double lat,
|
|
||||||
double lon, {
|
|
||||||
int w = 600,
|
|
||||||
int h = 360,
|
|
||||||
int zoom = 15,
|
|
||||||
int scale = 2,
|
|
||||||
String markerHex = 'E53935',
|
|
||||||
String maptype = 'roadmap',
|
|
||||||
String language = 'de',
|
|
||||||
String region = 'AT',
|
|
||||||
}) {
|
|
||||||
final mapsApiKey = dotenv.env['MAPS_KEY'] ?? '';
|
|
||||||
final qp = <String, String>{
|
|
||||||
'center': '$lat,$lon',
|
|
||||||
'zoom': '$zoom',
|
|
||||||
'size': '${w}x$h',
|
|
||||||
'scale': '$scale',
|
|
||||||
'maptype': maptype,
|
|
||||||
'format': 'png',
|
|
||||||
'language': language,
|
|
||||||
'region': region,
|
|
||||||
'markers': 'color:0x$markerHex|$lat,$lon',
|
|
||||||
'key': mapsApiKey,
|
|
||||||
};
|
|
||||||
return Uri.https('maps.googleapis.com', '/maps/api/staticmap', qp).toString();
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
||||||
import 'package:wien_talks_flutter/helper/go_router.dart';
|
import 'package:wien_talks_flutter/helper/go_router.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
void main() {
|
||||||
await dotenv.load(fileName: '.env');
|
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -13,7 +11,7 @@ class MyApp extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
title: 'Wien Talks',
|
title: 'Wien Talks FunMap',
|
||||||
theme: ThemeData(primarySwatch: Colors.green),
|
theme: ThemeData(primarySwatch: Colors.green),
|
||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,23 +11,24 @@ class NewsScreen extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var column = Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
HeadingText(text: "What's being said"),
|
|
||||||
LatestQuotesScreen(),
|
|
||||||
SizedBox(
|
|
||||||
height: 30,
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.pushNamed("create_event");
|
|
||||||
},
|
|
||||||
child: Text("Submit your own event")),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
return ScreenWidget(
|
return ScreenWidget(
|
||||||
child: LatestQuotesScreen(),
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
HeadingText(text: "What's being said"),
|
||||||
|
LatestQuotesScreen(),
|
||||||
|
SizedBox(
|
||||||
|
height: 30,
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.pushNamed("create_event");
|
||||||
|
},
|
||||||
|
child: Text("Submit your own event")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import 'dart:async';
|
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: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_util.dart';
|
|
||||||
import 'package:wien_talks_flutter/helper/time_util.dart';
|
import 'package:wien_talks_flutter/helper/time_util.dart';
|
||||||
import 'package:wien_talks_flutter/widgets/flamboyant_quote_card.dart';
|
import 'package:wien_talks_flutter/widgets/quote_card.dart';
|
||||||
|
|
||||||
class LatestQuotesScreen extends StatefulWidget {
|
class LatestQuotesScreen extends StatefulWidget {
|
||||||
const LatestQuotesScreen({super.key});
|
const LatestQuotesScreen({super.key});
|
||||||
|
|
@ -47,11 +45,10 @@ class _LatestQuotesScreenState extends State<LatestQuotesScreen> {
|
||||||
|
|
||||||
void _upsert(Quote q) {
|
void _upsert(Quote q) {
|
||||||
final i = _quotes.indexWhere((x) => x.id == q.id);
|
final i = _quotes.indexWhere((x) => x.id == q.id);
|
||||||
if (i >= 0) {
|
if (i >= 0)
|
||||||
_quotes[i] = q;
|
_quotes[i] = q;
|
||||||
} else {
|
else
|
||||||
_quotes.add(q);
|
_quotes.add(q);
|
||||||
}
|
|
||||||
_quotes.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
_quotes.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,12 +101,16 @@ class _LatestQuotesScreenState extends State<LatestQuotesScreen> {
|
||||||
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return MasonryGridView.count(
|
final unboundedHeight = constraints.maxHeight == double.infinity;
|
||||||
crossAxisCount: 2,
|
|
||||||
mainAxisSpacing: 8,
|
final list = ListView.separated(
|
||||||
crossAxisSpacing: 8,
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
|
shrinkWrap: unboundedHeight,
|
||||||
|
physics: unboundedHeight
|
||||||
|
? const NeverScrollableScrollPhysics()
|
||||||
|
: const AlwaysScrollableScrollPhysics(),
|
||||||
itemCount: _quotes.length,
|
itemCount: _quotes.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
final q = _quotes[i];
|
final q = _quotes[i];
|
||||||
final author = (q.authorName ?? '').trim();
|
final author = (q.authorName ?? '').trim();
|
||||||
|
|
@ -118,14 +119,18 @@ class _LatestQuotesScreenState extends State<LatestQuotesScreen> {
|
||||||
timeAgo(q.createdAt),
|
timeAgo(q.createdAt),
|
||||||
].join(' · ');
|
].join(' · ');
|
||||||
|
|
||||||
return FlamboyantQuoteCard(
|
return QuoteCard(
|
||||||
quote: q,
|
quote: q,
|
||||||
meta: meta,
|
meta: meta,
|
||||||
onVoteUp: () => _vote(q, true),
|
onVoteUp: () => _vote(q, true),
|
||||||
onVoteDown: () => _vote(q, false),
|
onVoteDown: () => _vote(q, false),
|
||||||
staticMapUrlBuilder: gStaticMap);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return unboundedHeight
|
||||||
|
? list
|
||||||
|
: RefreshIndicator(onRefresh: () async {}, child: list);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,224 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:wien_talks_client/wien_talks_client.dart';
|
|
||||||
import 'package:wien_talks_flutter/widgets/flamboyant_quote_card.dart';
|
|
||||||
import 'package:wien_talks_flutter/widgets/map_preview_widget.dart';
|
|
||||||
|
|
||||||
class CardContenty extends StatelessWidget {
|
|
||||||
const CardContenty({
|
|
||||||
super.key,
|
|
||||||
required this.quote,
|
|
||||||
required this.staticMapUrlBuilder,
|
|
||||||
required this.meta,
|
|
||||||
required this.onVoteUp,
|
|
||||||
required this.onVoteDown,
|
|
||||||
required this.context,
|
|
||||||
required this.variant,
|
|
||||||
required this.accent,
|
|
||||||
required this.metaStyle,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Quote quote;
|
|
||||||
final StaticMapUrlBuilder? staticMapUrlBuilder;
|
|
||||||
final String meta;
|
|
||||||
final VoidCallback onVoteUp;
|
|
||||||
final VoidCallback onVoteDown;
|
|
||||||
final BuildContext context;
|
|
||||||
final int variant;
|
|
||||||
final Color accent;
|
|
||||||
final TextStyle? metaStyle;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final hasMap = (variant != 0);
|
|
||||||
final map = hasMap
|
|
||||||
? Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
|
||||||
child: MapPreview(
|
|
||||||
lat: quote.lat,
|
|
||||||
lon: quote.long,
|
|
||||||
accent: accent,
|
|
||||||
staticMapUrlBuilder: staticMapUrlBuilder,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink();
|
|
||||||
|
|
||||||
final textBlock = _ContentText(
|
|
||||||
quote: quote,
|
|
||||||
meta: meta,
|
|
||||||
metaStyle: metaStyle,
|
|
||||||
onVoteUp: onVoteUp,
|
|
||||||
onVoteDown: onVoteDown,
|
|
||||||
accent: accent);
|
|
||||||
|
|
||||||
stacked() => Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [map, textBlock],
|
|
||||||
);
|
|
||||||
|
|
||||||
return switch (variant) {
|
|
||||||
1 when hasMap => stacked(),
|
|
||||||
2 when hasMap => LayoutBuilder(
|
|
||||||
builder: (context, c) {
|
|
||||||
final wide = c.maxWidth >= 420;
|
|
||||||
return wide
|
|
||||||
? Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
flex: 5,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(12, 10, 8, 12),
|
|
||||||
child: MapPreview(
|
|
||||||
lat: quote.lat,
|
|
||||||
lon: quote.long,
|
|
||||||
accent: accent,
|
|
||||||
staticMapUrlBuilder: staticMapUrlBuilder,
|
|
||||||
aspect: 4 / 3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Flexible(flex: 7, child: textBlock),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: stacked();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_ => textBlock,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ContentText extends StatelessWidget {
|
|
||||||
const _ContentText({
|
|
||||||
required this.quote,
|
|
||||||
required this.meta,
|
|
||||||
required this.metaStyle,
|
|
||||||
required this.onVoteUp,
|
|
||||||
required this.onVoteDown,
|
|
||||||
required this.accent,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Quote quote;
|
|
||||||
final String meta;
|
|
||||||
final TextStyle? metaStyle;
|
|
||||||
final VoidCallback onVoteUp;
|
|
||||||
final VoidCallback onVoteDown;
|
|
||||||
final Color accent;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 12),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
quote.text,
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(meta,
|
|
||||||
style: metaStyle, overflow: TextOverflow.ellipsis)),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
_VotePills(
|
|
||||||
up: quote.upvotes,
|
|
||||||
down: quote.downvotes,
|
|
||||||
onUp: onVoteUp,
|
|
||||||
onDown: onVoteDown,
|
|
||||||
accent: accent,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VotePills extends StatelessWidget {
|
|
||||||
const _VotePills({
|
|
||||||
required this.up,
|
|
||||||
required this.down,
|
|
||||||
required this.onUp,
|
|
||||||
required this.onDown,
|
|
||||||
required this.accent,
|
|
||||||
});
|
|
||||||
|
|
||||||
final int up;
|
|
||||||
final int down;
|
|
||||||
final VoidCallback onUp;
|
|
||||||
final VoidCallback onDown;
|
|
||||||
final Color accent;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final t = Theme.of(context);
|
|
||||||
final bg = t.colorScheme.surfaceContainerHighest.withValues(alpha: 0.55);
|
|
||||||
final onBg = ThemeData.estimateBrightnessForColor(bg) == Brightness.dark
|
|
||||||
? Colors.white
|
|
||||||
: const Color(0xFF1A1A1A);
|
|
||||||
final pillStyle = t.textTheme.labelSmall?.copyWith(color: onBg);
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
_Pill(
|
|
||||||
icon: Icons.arrow_upward,
|
|
||||||
color: accent,
|
|
||||||
text: '$up',
|
|
||||||
onTap: onUp,
|
|
||||||
textStyle: pillStyle,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
_Pill(
|
|
||||||
icon: Icons.arrow_downward,
|
|
||||||
color: const Color(0xFFD32F2F),
|
|
||||||
text: '$down',
|
|
||||||
onTap: onDown,
|
|
||||||
textStyle: pillStyle,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Pill extends StatelessWidget {
|
|
||||||
const _Pill({
|
|
||||||
required this.icon,
|
|
||||||
required this.color,
|
|
||||||
required this.text,
|
|
||||||
required this.onTap,
|
|
||||||
required this.textStyle,
|
|
||||||
});
|
|
||||||
|
|
||||||
final IconData icon;
|
|
||||||
final Color color;
|
|
||||||
final String text;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
final TextStyle? textStyle;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
color: color.withValues(alpha: 0.12),
|
|
||||||
borderRadius: BorderRadius.circular(999),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: onTap,
|
|
||||||
borderRadius: BorderRadius.circular(999),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 16, color: color),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(text, style: textStyle),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:wien_talks_client/wien_talks_client.dart';
|
|
||||||
import 'package:wien_talks_flutter/widgets/card_contenty.dart';
|
|
||||||
import 'package:wien_talks_flutter/widgets/ubahn_tape.dart';
|
|
||||||
|
|
||||||
typedef StaticMapUrlBuilder = String Function(
|
|
||||||
double lat,
|
|
||||||
double lon, {
|
|
||||||
int w,
|
|
||||||
int h,
|
|
||||||
int zoom,
|
|
||||||
});
|
|
||||||
|
|
||||||
class FlamboyantQuoteCard extends StatelessWidget {
|
|
||||||
const FlamboyantQuoteCard({
|
|
||||||
super.key,
|
|
||||||
required this.quote,
|
|
||||||
required this.meta,
|
|
||||||
required this.onVoteUp,
|
|
||||||
required this.onVoteDown,
|
|
||||||
this.staticMapUrlBuilder,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Quote quote;
|
|
||||||
final String meta;
|
|
||||||
final VoidCallback onVoteUp;
|
|
||||||
final VoidCallback onVoteDown;
|
|
||||||
final StaticMapUrlBuilder? staticMapUrlBuilder;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final seed = (quote.id ?? quote.text.hashCode) & 0x7fffffff;
|
|
||||||
final rng = math.Random(seed);
|
|
||||||
|
|
||||||
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), // red
|
|
||||||
const Color(0xFF3949AB), // indigo
|
|
||||||
const Color(0xFF00897B), // teal
|
|
||||||
];
|
|
||||||
final accent = accents[seed % accents.length];
|
|
||||||
|
|
||||||
final t = Theme.of(context);
|
|
||||||
final metaStyle = t.textTheme.bodySmall?.copyWith(
|
|
||||||
color: (t.textTheme.bodySmall?.color ?? t.colorScheme.onSurface)
|
|
||||||
.withValues(alpha: 0.70),
|
|
||||||
);
|
|
||||||
|
|
||||||
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(
|
|
||||||
padding: const EdgeInsets.only(top: 6, bottom: 2),
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: [
|
|
||||||
Transform.rotate(
|
|
||||||
angle: tiltRad,
|
|
||||||
child: card,
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: -8,
|
|
||||||
right: 16,
|
|
||||||
child: UbahnTape(
|
|
||||||
lat: quote.lat,
|
|
||||||
lon: quote.long,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:wien_talks_flutter/widgets/flamboyant_quote_card.dart';
|
|
||||||
|
|
||||||
class MapPreview extends StatelessWidget {
|
|
||||||
const MapPreview({
|
|
||||||
super.key,
|
|
||||||
required this.lat,
|
|
||||||
required this.lon,
|
|
||||||
required this.accent,
|
|
||||||
this.staticMapUrlBuilder,
|
|
||||||
this.aspect = 16 / 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
final double lat;
|
|
||||||
final double lon;
|
|
||||||
final double aspect;
|
|
||||||
final Color accent;
|
|
||||||
final StaticMapUrlBuilder? staticMapUrlBuilder;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final border = Border.all(color: accent.withValues(alpha: 0.25), width: 1);
|
|
||||||
final r = BorderRadius.circular(12);
|
|
||||||
|
|
||||||
final urlBuilder = staticMapUrlBuilder;
|
|
||||||
final w =
|
|
||||||
(MediaQuery.of(context).size.width / 2).clamp(280.0, 600.0).toInt();
|
|
||||||
final h = (w / aspect).round();
|
|
||||||
|
|
||||||
Widget content;
|
|
||||||
if (urlBuilder != null) {
|
|
||||||
final url = urlBuilder(lat, lon, w: w, h: h, zoom: 15);
|
|
||||||
|
|
||||||
content = ClipRRect(
|
|
||||||
borderRadius: r,
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: url,
|
|
||||||
width: double.infinity,
|
|
||||||
height: h.toDouble(),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
content =
|
|
||||||
_MapPlaceholder(accent: accent, height: h.toDouble(), radius: r);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DecoratedBox(
|
|
||||||
decoration: BoxDecoration(border: border, borderRadius: r),
|
|
||||||
child: content,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MapPlaceholder extends StatelessWidget {
|
|
||||||
const _MapPlaceholder({required this.accent, this.height, this.radius});
|
|
||||||
|
|
||||||
final Color accent;
|
|
||||||
final double? height;
|
|
||||||
final BorderRadius? radius;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final t = Theme.of(context);
|
|
||||||
return ClipRRect(
|
|
||||||
borderRadius: radius ?? BorderRadius.circular(12),
|
|
||||||
child: Container(
|
|
||||||
height: height,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
t.colorScheme.surfaceContainerHighest.withValues(alpha: 0.50),
|
|
||||||
t.colorScheme.surfaceContainerHighest.withValues(alpha: 0.20),
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.location_on, color: accent),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text('Map preview', style: t.textTheme.labelMedium),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,7 +13,6 @@ class NewsInputForm extends StatefulWidget {
|
||||||
const NewsInputForm({super.key, required this.onSubmit});
|
const NewsInputForm({super.key, required this.onSubmit});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// ignore: library_private_types_in_public_api
|
|
||||||
_NewsInputFormState createState() => _NewsInputFormState();
|
_NewsInputFormState createState() => _NewsInputFormState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +45,7 @@ class _NewsInputFormState extends State<NewsInputForm> {
|
||||||
try {
|
try {
|
||||||
await widget.onSubmit(newsData);
|
await widget.onSubmit(newsData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (mounted) {
|
if (context.mounted) {
|
||||||
ErrorSnackbar().show(context, error.toString());
|
ErrorSnackbar().show(context, error.toString());
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ class ScreenWidget extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('FunMap'),
|
||||||
|
),
|
||||||
floatingActionButton: AddQuoteFab(),
|
floatingActionButton: AddQuoteFab(),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|
|
||||||
|
|
@ -1,193 +0,0 @@
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class UbahnTape extends StatelessWidget {
|
|
||||||
const UbahnTape({
|
|
||||||
super.key,
|
|
||||||
this.lat,
|
|
||||||
this.lon,
|
|
||||||
this.rotationDeg = 6,
|
|
||||||
this.maxLinesShown = 1,
|
|
||||||
this.stations = kViennaStationsSample,
|
|
||||||
});
|
|
||||||
|
|
||||||
final double? lat;
|
|
||||||
final double? lon;
|
|
||||||
final double rotationDeg;
|
|
||||||
final int maxLinesShown;
|
|
||||||
final List<UbahnStation> stations;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final lines = _resolveLines();
|
|
||||||
final primary = lines.isNotEmpty
|
|
||||||
? (kUbahnLineColors[lines.first] ?? _kNeutral)
|
|
||||||
: _kNeutral;
|
|
||||||
|
|
||||||
final bg = primary.withValues(alpha: 0.30);
|
|
||||||
|
|
||||||
return Transform.rotate(
|
|
||||||
angle: rotationDeg * math.pi / 180,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: bg,
|
|
||||||
borderRadius: BorderRadius.circular(3),
|
|
||||||
boxShadow: const [
|
|
||||||
BoxShadow(
|
|
||||||
color: Color(0x33000000),
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
border: Border.all(
|
|
||||||
color: primary.withValues(alpha: 0.35),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 14,
|
|
||||||
height: 14,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _uBlue,
|
|
||||||
borderRadius: BorderRadius.circular(3),
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'U',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
fontSize: 10,
|
|
||||||
height: 1.0,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
if (lines.isNotEmpty)
|
|
||||||
...lines.take(maxLinesShown).expand((line) => [
|
|
||||||
_LineChip(line: line),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> _resolveLines() {
|
|
||||||
if (lat == null || lon == null) return const [];
|
|
||||||
final nearest = _nearestStation(stations, lat!, lon!);
|
|
||||||
return nearest?.lines ?? const [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LineChip extends StatelessWidget {
|
|
||||||
const _LineChip({required this.line});
|
|
||||||
final String line;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final color = kUbahnLineColors[line] ?? _kNeutral;
|
|
||||||
final on = _onColor(color);
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: color,
|
|
||||||
borderRadius: BorderRadius.circular(999),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
line,
|
|
||||||
style: TextStyle(
|
|
||||||
color: on,
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
height: 1.0,
|
|
||||||
letterSpacing: 0.2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _onColor(Color bg) {
|
|
||||||
return ThemeData.estimateBrightnessForColor(bg) == Brightness.dark
|
|
||||||
? Colors.white
|
|
||||||
: const Color(0xFF111111);
|
|
||||||
}
|
|
||||||
|
|
||||||
const _uBlue = Color(0xFF1E88E5); // Vienna U sign-ish blue
|
|
||||||
const _kNeutral = Color(0xFF9E9E9E);
|
|
||||||
|
|
||||||
// Official-ish line colors
|
|
||||||
const Map<String, Color> kUbahnLineColors = {
|
|
||||||
'U1': Color(0xFFE20613), // red
|
|
||||||
'U2': Color(0xFFA762A3), // purple
|
|
||||||
'U3': Color(0xFFF29400), // orange
|
|
||||||
'U4': Color(0xFF009640), // green
|
|
||||||
'U5': Color(0xFF63318F), // violet (future)
|
|
||||||
'U6': Color(0xFF8D5B2D), // brown
|
|
||||||
};
|
|
||||||
|
|
||||||
class UbahnStation {
|
|
||||||
const UbahnStation(this.name, this.lat, this.lon, this.lines);
|
|
||||||
final String name;
|
|
||||||
final double lat;
|
|
||||||
final double lon;
|
|
||||||
final List<String> lines; // e.g., ['U1','U3']
|
|
||||||
}
|
|
||||||
|
|
||||||
UbahnStation? _nearestStation(
|
|
||||||
List<UbahnStation> stations,
|
|
||||||
double lat,
|
|
||||||
double lon,
|
|
||||||
) {
|
|
||||||
if (stations.isEmpty) return null;
|
|
||||||
UbahnStation best = stations.first;
|
|
||||||
double bestD = _haversine(best.lat, best.lon, lat, lon);
|
|
||||||
for (var i = 1; i < stations.length; i++) {
|
|
||||||
final s = stations[i];
|
|
||||||
final d = _haversine(s.lat, s.lon, lat, lon);
|
|
||||||
if (d < bestD) {
|
|
||||||
best = s;
|
|
||||||
bestD = d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
double _haversine(double lat1, double lon1, double lat2, double lon2) {
|
|
||||||
const R = 6371000.0;
|
|
||||||
final dLat = (lat2 - lat1) * (math.pi / 180.0);
|
|
||||||
final dLon = (lon2 - lon1) * (math.pi / 180.0);
|
|
||||||
final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
|
||||||
math.cos(lat1 * (math.pi / 180.0)) *
|
|
||||||
math.cos(lat2 * (math.pi / 180.0)) *
|
|
||||||
math.sin(dLon / 2) *
|
|
||||||
math.sin(dLon / 2);
|
|
||||||
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
|
||||||
return R * c;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compact central sample; swap in full dataset when ready
|
|
||||||
const List<UbahnStation> kViennaStationsSample = [
|
|
||||||
UbahnStation('Stephansplatz', 48.2084, 16.3731, ['U1', 'U3']),
|
|
||||||
UbahnStation('Karlsplatz', 48.2000, 16.3690, ['U1', 'U2', 'U4']),
|
|
||||||
UbahnStation('Schwedenplatz', 48.2111, 16.3776, ['U1', 'U4']),
|
|
||||||
UbahnStation('Praterstern', 48.2169, 16.3909, ['U1', 'U2']),
|
|
||||||
UbahnStation('Schottenring', 48.2152, 16.3720, ['U2', 'U4']),
|
|
||||||
UbahnStation('Volkstheater', 48.2078, 16.3604, ['U2', 'U3']),
|
|
||||||
UbahnStation('Museumsquartier', 48.2026, 16.3614, ['U2']),
|
|
||||||
UbahnStation('Westbahnhof', 48.1967, 16.3378, ['U3', 'U6']),
|
|
||||||
UbahnStation('Wien Mitte/Landstraße', 48.2070, 16.3834, ['U3', 'U4']),
|
|
||||||
UbahnStation('Spittelau', 48.2409, 16.3585, ['U4', 'U6']),
|
|
||||||
UbahnStation('Längenfeldgasse', 48.1848, 16.3299, ['U4', 'U6']),
|
|
||||||
UbahnStation('Erdberg', 48.1907, 16.4196, ['U3']),
|
|
||||||
UbahnStation('Kaisermühlen VIC', 48.2348, 16.4130, ['U1']),
|
|
||||||
UbahnStation('Floridsdorf', 48.2570, 16.4030, ['U6']),
|
|
||||||
UbahnStation('Ottakring', 48.2120, 16.3080, ['U3']),
|
|
||||||
];
|
|
||||||
|
|
@ -52,9 +52,6 @@ dependencies:
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
google_sign_in: ^7.1.1
|
google_sign_in: ^7.1.1
|
||||||
flutter_staggered_grid_view: ^0.7.0
|
|
||||||
cached_network_image: ^3.4.1
|
|
||||||
flutter_dotenv: ^5.2.1
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: '>=3.0.0 <7.0.0'
|
flutter_lints: '>=3.0.0 <7.0.0'
|
||||||
|
|
@ -74,7 +71,6 @@ flutter:
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
assets:
|
assets:
|
||||||
- .env
|
|
||||||
- assets/funny_images/
|
- assets/funny_images/
|
||||||
- assets/render_themes/
|
- assets/render_themes/
|
||||||
- packages/mapsforge_flutter/assets/patterns/dark_farmland.svg
|
- packages/mapsforge_flutter/assets/patterns/dark_farmland.svg
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue