Merge pull request #2 from timokz/refactor/flatten-layout

Refactor/flatten layout
This commit is contained in:
Max R. Carrara 2025-08-17 12:16:42 +02:00 committed by GitHub
commit ce19e1ae75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
309 changed files with 1096 additions and 521 deletions

46
README.md Normal file
View file

@ -0,0 +1,46 @@
# Wien Talks
**An app developed during the [2nd Flutter Vienna Hackathon](https://www.meetup.com/fluttervienna/events/310014357/).**
## Team
_Grätzel-Goblins_ — Timo, Max & Mike
## Description
_Wien Talks_ is an app to share the iconic and quotable lines you hear Viennese
people drop throughout their day. It doesn't matter if it's a U6 train
conductor's insults that he made during an announcement or the Spar cashier's
funny retort to "Zweite Kassa bitte!", _Wien Talks_ is made to record, preserve
and share Viennese quotes with others.
Quotes are community-moderated—users can up- or down-vote posts. Additionally,
when creating a new quote, the location from which the user made it is added,
too. This allows you to see what's going on near you.
## Screenshots
<table>
<tr>
<td align="center">
<img src="unicorn/Screenshot_1755421875.png" width="300" alt="Default quotes list"/>
<div><sub>Default quotes view</sub></div>
</td>
<td align="center">
<img src="unicorn/Screenshot_1755421892.png" width="300" alt="Filtered quotes list"/>
<div><sub>Filtered quotes</sub></div>
</td>
</tr>
<tr>
<td align="center">
<img src="unicorn/Screenshot_1755421897.png" width="300" alt="Reuse location from quote"/>
<div><sub>Tap a card → create with same location</sub></div>
</td>
<td align="center">
<img src="unicorn/Screenshot_1755421907.png" width="300" alt="Custom location picker"/>
<div><sub>Set a custom location</sub></div>
</td>
</tr>
</table>
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,011 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

View file

@ -1,75 +0,0 @@
name: Deploy to AWS
on:
push:
branches: [ deployment-aws-production, deployment-aws-staging ]
workflow_dispatch:
inputs:
target:
description: 'Target'
required: true
default: 'production'
type: choice
options:
- 'staging'
- 'production'
jobs:
deploy:
name: Deploy to AWS
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: recursive
- name: Setup Dart SDK
uses: dart-lang/setup-dart@v1.6.5
with:
sdk: 3.5
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: Create passwords file
working-directory: wien_talks_server
shell: bash
env:
SERVERPOD_PASSWORDS: ${{ secrets.SERVERPOD_PASSWORDS }}
run: |
pwd
echo "$SERVERPOD_PASSWORDS" > config/passwords.yaml
ls config/
- name: Get Dart packages
working-directory: wien_talks_server
run: dart pub get
- name: Compile server
working-directory: wien_talks_server
run: dart compile kernel bin/main.dart
- name: Create CodeDeploy Deployment
id: deploy
env:
PROJECT_NAME: wien_talks
AWS_NAME: wien-talks
DEPLOYMENT_BUCKET: wien-talks-deployment-6559518
TARGET: ${{ github.event.inputs.target }}
run: |
# Deploy server to AWS
TARGET="${TARGET:=${GITHUB_REF##*-}}"
echo "Deploying to target: $TARGET"
mkdir -p vendor
cp "${PROJECT_NAME}_server/deploy/aws/scripts/appspec.yml" appspec.yml
zip -r deployment.zip .
aws s3 cp deployment.zip "s3://${DEPLOYMENT_BUCKET}/deployment.zip"
aws deploy create-deployment \
--application-name "${AWS_NAME}-app" \
--deployment-group-name "${AWS_NAME}-${TARGET}-group" \
--deployment-config-name CodeDeployDefault.OneAtATime \
--s3-location "bucket=${DEPLOYMENT_BUCKET},key=deployment.zip,bundleType=zip"

View file

@ -1,99 +0,0 @@
name: Deploy to GCP
on:
push:
branches: [ deployment-gcp-production, deployment-gcp-staging ]
workflow_dispatch:
inputs:
target:
description: 'Target'
required: true
default: 'production'
type: choice
options:
- 'staging'
- 'production'
env:
# TODO: Update with your Google Cloud project id. If you have changed the
# region and zone in your Terraform configuration, you will need to change
# it here too.
PROJECT: "<PROJECT ID>"
REGION: us-central1
ZONE: us-central1-c
jobs:
deploy:
name: Deploy to Google Cloud Run
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: recursive
- name: Setting Target Mode from Input
if: ${{ github.event.inputs.target != '' }}
run: echo "TARGET=${{ github.event.inputs.target }}" >> $GITHUB_ENV
- name: Setting Target mode based on branch
if: ${{ github.event.inputs.target == '' }}
run: echo "TARGET=${GITHUB_REF##*-}" >> $GITHUB_ENV
- name: Set repository
run: echo "REPOSITORY=serverpod-${{ env.TARGET }}-container" >> $GITHUB_ENV
- name: Set Image Name
run: echo "IMAGE_NAME=serverpod" >> $GITHUB_ENV
- name: Set Service Name
run: echo "SERVICE_NAME=$(echo $IMAGE_NAME | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
- name: Test
run: echo $SERVICE_NAME
- id: "auth"
name: "Authenticate to Google Cloud"
uses: "google-github-actions/auth@v1"
with:
credentials_json: "${{ secrets.GOOGLE_CREDENTIALS }}"
- name: Create passwords file
working-directory: wien_talks_server
shell: bash
env:
SERVERPOD_PASSWORDS: ${{ secrets.SERVERPOD_PASSWORDS }}
run: |
pwd
echo "$SERVERPOD_PASSWORDS" > config/passwords.yaml
ls config/
- name: Configure Docker
working-directory: wien_talks_server
run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev
- name: Build the Docker image
working-directory: wien_talks_server
run: "docker build -t $IMAGE_NAME ."
- name: Tag the Docker image
working-directory: wien_talks_server
run: docker tag $IMAGE_NAME ${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT }}/${{ env.REPOSITORY }}/$IMAGE_NAME
- name: Push Docker image
working-directory: wien_talks_server
run: docker push ${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT }}/${{ env.REPOSITORY }}/$IMAGE_NAME
# Uncomment the following code to automatically restart the servers in the
# instance group when you push a new version of your code. Before doing
# this, make sure that you have successfully deployed a first version.
#
# - name: Restart servers in instance group
# run: |
# gcloud compute instance-groups managed rolling-action replace serverpod-${{ env.TARGET }}-group \
# --project=${{ env.PROJECT }} \
# --replacement-method='substitute' \
# --max-surge=1 \
# --max-unavailable=1 \
# --zone=${{ env.ZONE }}

View file

@ -1,44 +0,0 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.wien_talks"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.wien_talks"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View file

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

View file

@ -1,40 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:location/location.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/mapfile_widget.dart';
import 'package:wien_talks_flutter/widgets/news_input_form.dart';
import 'package:wien_talks_flutter/widgets/screen_widget.dart';
import '../helper/location_mgr.dart';
class CreateEventScreen extends StatelessWidget {
const CreateEventScreen({super.key});
@override
Widget build(BuildContext context) {
return ScreenWidget(
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(),
),
),
],
));
}
}

View file

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

View file

@ -1,132 +0,0 @@
import 'dart:async';
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_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/widgets/flamboyant_quote_card.dart';
class LatestQuotesScreen extends StatefulWidget {
const LatestQuotesScreen({super.key});
@override
State<LatestQuotesScreen> createState() => _LatestQuotesScreenState();
}
class _LatestQuotesScreenState extends State<LatestQuotesScreen> {
final List<Quote> _quotes = [];
StreamSubscription<Quote>? _sub;
Object? _error;
@override
void initState() {
super.initState();
_connectStream();
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
void _connectStream() {
_sub?.cancel();
_sub = FunmapMgr().client.quote.streamAllQuotes(limit: 50).listen(
(q) => setState(() => _upsert(q)),
onError: (e) => setState(() => _error = e),
onDone: () => Future.delayed(const Duration(seconds: 2), () {
if (mounted) _connectStream();
}),
cancelOnError: false,
);
}
void _upsert(Quote q) {
final i = _quotes.indexWhere((x) => x.id == q.id);
if (i >= 0) {
_quotes[i] = q;
} else {
_quotes.add(q);
}
_quotes.sort((a, b) => b.createdAt.compareTo(a.createdAt));
}
void _sortDesc() {
_quotes.sort((a, b) => b.createdAt.compareTo(a.createdAt));
}
Future<void> _vote(Quote quote, bool up) async {
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(() {
_quotes[idx] = updated;
_sortDesc();
});
try {
await FunmapMgr().client.quote.updateQuote(updated);
} catch (e) {
if (!mounted) return;
setState(() => _quotes[idx] = original);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Vote failed: $e')),
);
}
}
@override
Widget build(BuildContext context) {
if (_quotes.isEmpty && _error == null) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null && _quotes.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('Error: $_error'),
),
);
}
if (_quotes.isEmpty) {
return const Center(child: Text('Nix da. Sag halt was'));
}
return LayoutBuilder(
builder: (context, constraints) {
return MasonryGridView.count(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
itemCount: _quotes.length,
itemBuilder: (context, i) {
final q = _quotes[i];
final author = (q.authorName ?? '').trim();
final meta = [
if (author.isNotEmpty) author,
timeAgo(q.createdAt),
].join(' · ');
return FlamboyantQuoteCard(
quote: q,
meta: meta,
onVoteUp: () => _vote(q, true),
onVoteDown: () => _vote(q, false),
staticMapUrlBuilder: gStaticMap);
},
);
},
);
}
}

View file

@ -1,26 +0,0 @@
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
class CarouselWidget extends StatelessWidget {
const CarouselWidget({super.key});
@override
Widget build(BuildContext context) {
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

@ -44,6 +44,7 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
android/key.properties
.env .env

View file

@ -0,0 +1,72 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
}
dependencies {
implementation("com.google.android.play:core:1.10.3")
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) }
}
android {
namespace = "com.wien_talks"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions { jvmTarget = "17" }
signingConfigs {
if (keystorePropertiesFile.exists()) {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
val storePath = keystoreProperties["storeFile"] as String?
storeFile = storePath?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
}
defaultConfig {
applicationId = "com.wien_talks"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
signingConfig = signingConfigs.findByName("release")
?: signingConfigs.getByName("debug")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
debug {
}
}
}
flutter {
source = "../.."
}

View file

@ -0,0 +1,8 @@
-keep class io.flutter.embedding.** { *; }
-keep class io.flutter.plugins.** { *; }
-keep class io.flutter.** { *; }
-keep class com.google.android.gms.** { *; }
-dontwarn com.google.android.gms.**
-dontwarn kotlinx.coroutines.**

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Some files were not shown because too many files have changed in this diff Show more