glanzlicht / Meinweimar_Saving_App

MW Rettertuete & Deals - clean

Verified secure showcase
112 Files 341 KB Public size 7 Redacted Dart · XCCONFIG · H · XML · GITIGNORE · PLIST

Project overview

Meinweimar_Saving_App is a verified code showcase with 112 visible project entries across 9 top-level modules. The primary detected language is Dart; protected files remain visible in the structure while their contents stay private.

README

meinweimar_box__nativ

A new Flutter project.

Getting Started

This project is a starting point for a Flutter application.

A few resources to get you started if this is your first Flutter project:

For help getting started with Flutter development, view the online documentation, which offers tutorials, samples, guidance on mobile development, and a full API reference.

lib/product_detail_page.dartDart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import 'constants.dart';
import 'main.dart';
import 'checkout_page.dart';
import 'login_page.dart';
import 'VendorDetailPage.dart';
import 'core/api/api_endpoints.dart';
import 'core/api/auth_headers.dart';
import 'core/prefs/pref_keys.dart';
class ProductDetailPage extends StatefulWidget {
final dynamic product;
const ProductDetailPage({super.key, required this.product});
@override
State<ProductDetailPage> createState() => _ProductDetailPageState();
}
class _ProductDetailPageState extends State<ProductDetailPage> with RouteAware {
Timer? _timer;
Duration _timeLeft = Duration.zero;
String _statusTitle = "Lädt...";
String _todayRangeText = "Heute keine Abholung";
bool _isExpired = false;
bool _isLoggedIn = false;
Favoriten-State für Vendor
bool _isVendorFavorite = false;
bool _isFavLoading = false;
@override
void initState() {
super.initState();
_checkLoginStatus();
_startTimer();
}
RouteAware subscribe (Variante B)
@override
void didChangeDependencies() {
super.didChangeDependencies();
final route = ModalRoute.of(context);
if (route is PageRoute) {
routeObserver.subscribe(this, route);
}
}
@override
void dispose() {
routeObserver.unsubscribe(this);
_timer?.cancel();
super.dispose();
}
Wird aufgerufen, wenn man von einer anderen Seite zurückkommt (Variante B)
@override
void didPopNext() {
_checkLoginStatus();
}
Future<void> _checkLoginStatus() async {
final prefs = await SharedPreferences.getInstance();
final String? token = prefs.getString(PrefKeys.jwtToken);
if (mounted) {
setState(() {
_isLoggedIn = (token != null && token.isNotEmpty);
});
}
Favoriten-State nach Loginstatus laden
final vendorId = _getVendorIdFromProduct();
if (vendorId > 0 && token != null && token.isNotEmpty) {
await _fetchVendorFavoriteState(vendorId);
} else {
if (mounted) setState(() => _isVendorFavorite = false);
}
}
int _getVendorIdFromProduct() {
try {
final v = widget.product['vendor_info'];
if (v != null && v is Map) {
return int.tryParse(v['vendor_id']?.toString() ?? "0") ?? 0;
}
} catch (_) {}
return 0;
}
void _showLoginRequiredFavoriteBubble() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: false,
builder: (ctx) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Container(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.12),
blurRadius: 18,
offset: const Offset(0, 8),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppConstants.mainColor.withOpacity(0.10),
shape: BoxShape.circle,
),
child: const Icon(Icons.favorite, color: AppConstants.mainColor, size: 18),
),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Favoriten",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: AppConstants.fontGrey,
),
),
const SizedBox(height: 6),
const Text(
"Bitte logge dich ein, um Händler zu deinen Favoriten hinzuzufügen.",
style: TextStyle(fontSize: 13, color: AppConstants.fontGrey, height: 1.3),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(ctx),
style: OutlinedButton.styleFrom(
foregroundColor: AppConstants.fontGrey,
side: BorderSide(color: Colors.grey.withOpacity(0.35)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text("Später"),
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const LoginPage()),
Variante A
},
style: ElevatedButton.styleFrom(
backgroundColor: AppConstants.mainColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 0,
),
child: const Text("Einloggen", style: TextStyle(color: Colors.white)),
),
),
],
),
],
),
),
],
),
),
),
);
},
);
}
Future<void> _fetchVendorFavoriteState(int vendorId) async {
if (_isFavLoading) return;
setState(() => _isFavLoading = true);
try {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString(PrefKeys.jwtToken);
if (token == null || token.isEmpty) {
if (mounted) setState(() => _isVendorFavorite = false);
return;
}
final url = ApiEndpoints.myFavorites;
final res = await http.get(
Uri.parse(url),
headers: AuthHeaders.bearer(token),
).timeout(const Duration(seconds: 8));
if (res.statusCode == 200) {
final decoded = json.decode(res.body);
bool found = false;
if (decoded is List) {
for (final item in decoded) {
if (item is int) {
if (item == vendorId) {
found = true;
break;
}
} else if (item is String) {
if (int.tryParse(item) == vendorId) {
found = true;
break;
}
} else if (item is Map) {
final id = int.tryParse(item['vendor_id']?.toString() ?? item['id']?.toString() ?? "0") ?? 0;
if (id == vendorId) {
found = true;
break;
}
}
}
}
if (mounted) setState(() => _isVendorFavorite = found);
}
} catch (_) {
// bewusst still
} finally {
if (mounted) setState(() => _isFavLoading = false);
}
}
Future<void> _toggleVendorFavorite(int vendorId) async {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString(PrefKeys.jwtToken);
if (token == null || token.isEmpty) {
_showLoginRequiredFavoriteBubble();
return;
}
// UI sofort reagieren lassen
final previous = _isVendorFavorite;
if (mounted) setState(() => _isVendorFavorite = !previous);
try {
final url = ApiEndpoints.toggleFavorite(vendorId.toString());
final res = await http.post(
Uri.parse(url),
headers: AuthHeaders.bearer(token),
).timeout(const Duration(seconds: 8));
if (res.statusCode != 200) {
// rollback
if (mounted) setState(() => _isVendorFavorite = previous);
} else {
Server-Truth nachziehen
_fetchVendorFavoriteState(vendorId);
}
} catch (_) {
// rollback
if (mounted) setState(() => _isVendorFavorite = previous);
}
}
void _startTimer() {
_updateTime();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) => _updateTime());
}
String? _getMetaValue(String key) {
if (widget.product['meta_data'] == null) return null;
final meta = widget.product['meta_data'] as List;
final entry = meta.firstWhere(
(m) => m['key'] == key || m['key'] == '_$key',
orElse: () => null,
);
return entry?['value']?.toString();
}
void _updateTime() {
final now = DateTime.now();
final weekdayMap = {
DateTime.monday: {'slug': 'montags', 'prefix': 'monday'},
DateTime.tuesday: {'slug': 'dienstags', 'prefix': 'tuesday'},
DateTime.wednesday: {'slug': 'mittwochs', 'prefix': 'wednesday'},
DateTime.thursday: {'slug': 'donnerstags', 'prefix': 'thursday'},
DateTime.friday: {'slug': 'freitags', 'prefix': 'friday'},
DateTime.saturday: {'slug': 'samstags', 'prefix': 'saturday'},
DateTime.sunday: {'slug': 'sonntags', 'prefix': 'sunday'}
};
final mapping = weekdayMap[now.weekday]!;
final String keyVon = "abholzeiten_${mapping['slug']}_${mapping['prefix']}_von";
final String keyBis = "abholzeiten_${mapping['slug']}_${mapping['prefix']}_bis";
final String? vonStr = _getMetaValue(keyVon);
final String? bisStr = _getMetaValue(keyBis);
if (vonStr == null || bisStr == null || vonStr.isEmpty || bisStr.isEmpty) {
if (mounted) {
setState(() {
_statusTitle = "Heute keine Abholung";
_todayRangeText = "Keine Abholzeit hinterlegt";
_isExpired = true;
});
}
return;
}
try {
final startParts = vonStr.split(':');
final endParts = bisStr.split(':');
final startTime = DateTime(now.year, now.month, now.day, int.parse(startParts[0]), int.parse(startParts[1]));
final endTime = DateTime(now.year, now.month, now.day, int.parse(endParts[0]), int.parse(endParts[1]));
if (mounted) {
setState(() {
_todayRangeText = "Abholung HEUTE: ${vonStr.substring(0, 5)} - ${bisStr.substring(0, 5)} Uhr";
if (now.isBefore(startTime)) {
_statusTitle = "Abholung startet in:";
_timeLeft = startTime.difference(now);
_isExpired = false;
} else if (now.isAfter(startTime) && now.isBefore(endTime)) {
_statusTitle = "Verbleibende Abholzeit:";
_timeLeft = endTime.difference(now);
_isExpired = false;
} else {
_statusTitle = "Aktion für heute beendet";
_isExpired = true;
_timer?.cancel();
}
});
}
} catch (e) {
debugPrint("Fehler beim Parsen der ACF Zeiten: $e");
}
}
@override
Widget build(BuildContext context) {
var v = widget.product['vendor_info'];
var p = widget.product;
double rating = 0.0;
try {
if (v != null && v is Map) {
var finalVRating = v['store_rating'] ?? v['avg_review_rating'] ?? v['vendor_rating'];
rating = double.tryParse(finalVRating?.toString() ?? "0.0") ?? 0.0;
}
if (rating == 0) {
rating = double.tryParse(p['average_rating']?.toString() ?? "0.0") ?? 0.0;
}
} catch (e) {
rating = 0.0;
}
VendorId für Herzbutton
final int vendorId = _getVendorIdFromProduct();
return Scaffold(
backgroundColor: AppConstants.baseWhite,
appBar: AppBar(
backgroundColor: AppConstants.baseWhite,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: AppConstants.fontGrey),
onPressed: () => Navigator.pop(context),
),
title: Text(
v?['store_name'] ?? "Details",
style: const TextStyle(color: AppConstants.fontGrey, fontWeight: FontWeight.bold),
),
Favoriten-Button (Händler merken)
actions: [
if (vendorId > 0)
IconButton(
onPressed: _isFavLoading ? null : () => _toggleVendorFavorite(vendorId),
icon: Icon(
_isVendorFavorite ? Icons.favorite : Icons.favorite_border,
color: AppConstants.mainColor,
),
),
],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
Image.network(p['images'][0]['src'], width: double.infinity, height: 300, fit: BoxFit.cover),
if (v != null && v['logo'] != "")
Positioned(
top: 20,
left: 20,
child: GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => VendorDetailPage(vendor: v))),
child: Container(
padding: const EdgeInsets.all(3),
decoration: const BoxDecoration(color: AppConstants.baseWhite, shape: BoxShape.circle),
child: CircleAvatar(backgroundImage: NetworkImage(v['logo']), radius: 35),
),
),
),
],
),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(p['name'], style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: AppConstants.fontGrey)),
const SizedBox(height: 5),
GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => VendorDetailPage(vendor: v))),
child: Row(
children: [
Text(
"Verkäufer: ${v?['store_name'] ?? 'Partner'}",
style: const TextStyle(fontSize: 13, color: AppConstants.fontGrey, decoration: TextDecoration.underline),
),
if (rating > 0) ...[
const SizedBox(width: 10),
const Icon(Icons.star, color: Colors.amber, size: 14),
const SizedBox(width: 4),
Text(
rating.toStringAsFixed(1),
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: AppConstants.fontGrey),
),
],
],
),
),
const SizedBox(height: 15),
Row(
children: [
Text("${formatPrice(p['price'])} €", style: const TextStyle(fontSize: 22, color: AppConstants.mainColor, fontWeight: FontWeight.bold)),
if (p['regular_price'] != p['price'] && p['regular_price'] != "") ...[
const SizedBox(width: 10),
Text("${formatPrice(p['regular_price'])} €", style: const TextStyle(fontSize: 16, color: Colors.grey, decoration: TextDecoration.lineThrough)),
]
],
),
const Divider(height: 40),
const Text("Abholzeit", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: AppConstants.fontGrey)),
const SizedBox(height: 4),
Text(_todayRangeText, style: const TextStyle(fontSize: 15, color: AppConstants.fontGrey)),
const SizedBox(height: 30),
Text(_statusTitle, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: AppConstants.fontGrey)),
const SizedBox(height: 15),
if (!_isExpired)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_timerBox(_timeLeft.inHours.toString().padLeft(2, '0'), "Stunden"),
_timerBox((_timeLeft.inMinutes % 60).toString().padLeft(2, '0'), "Minuten"),
_timerBox((_timeLeft.inSeconds % 60).toString().padLeft(2, '0'), "Sekunden"),
],
)
else
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15)),
child: Center(child: Text(_statusTitle, style: const TextStyle(color: AppConstants.fontGrey, fontWeight: FontWeight.bold))),
),
const Divider(height: 60),
const Text("Beschreibung", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(height: 5),
Text(p['description'].toString().replaceAll(RegExp(r'<[^>]*>'), ''), style: const TextStyle(color: AppConstants.fontGrey, fontSize: 15, height: 1.5)),
const SizedBox(height: 30),
],
),
),
],
),
),
bottomNavigationBar: _isExpired
? null
: Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(color: AppConstants.baseWhite, border: Border(top: BorderSide(color: Colors.black12))),
child: ElevatedButton(
onPressed: () async {
if (_isLoggedIn) {
Navigator.push(context, MaterialPageRoute(builder: (context) => CheckoutPage(product: p)));
} else {
await Navigator.push(context, MaterialPageRoute(builder: (context) => const LoginPage()));
_checkLoginStatus();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppConstants.mainColor,
minimumSize: const Size(double.infinity, 55),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
child: Text(
_isLoggedIn ? "TÜTE JETZT RETTEN" : "ANMELDEN ZUM RETTEN",
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16),
),
),
),
);
}
Widget _timerBox(String value, String label) {
return Container(
width: MediaQuery.of(context).size.width * 0.28,
padding: const EdgeInsets.symmetric(vertical: 15),
decoration: BoxDecoration(color: AppConstants.fontGrey, borderRadius: BorderRadius.circular(15)),
child: Column(
children: [
Text(value, style: const TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold)),
Text(label, style: const TextStyle(color: Colors.white, fontSize: 12)),
],
),
);
}
}

Architecture

High-level modules detected from the verified public snapshot. Protected implementation details are intentionally omitted.

macosmodule
iosmodule
libmodule
windowsmodule
androidmodule
linuxmodule
webmodule
testmodule

Technology stack

DartPBXPROJLOCKXIBTXTCPPFlutter / Dart
Dart39.9%
PBXPROJ16.4%
LOCK7.1%
XIB6.8%
TXT5.2%
CPP3.9%
JSON2.9%
XCSCHEME2.2%