Compare commits
1 Commits
build-2026
...
build-2026
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75a3a8dd60 |
280
lib/main.dart
280
lib/main.dart
@@ -19,9 +19,59 @@ class KorkenMosaicApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const seed = Color(0xFF26BFD6);
|
||||
final colorScheme = ColorScheme.fromSeed(
|
||||
seedColor: seed,
|
||||
brightness: Brightness.light,
|
||||
);
|
||||
|
||||
return MaterialApp(
|
||||
title: 'Korken Mosaic',
|
||||
theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: colorScheme,
|
||||
scaffoldBackgroundColor: const Color(0xFFEAF9FF),
|
||||
textTheme: ThemeData.light().textTheme.copyWith(
|
||||
titleLarge: const TextStyle(fontWeight: FontWeight.w700),
|
||||
titleMedium: const TextStyle(fontWeight: FontWeight.w600),
|
||||
bodyLarge: const TextStyle(height: 1.3),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 0,
|
||||
color: Colors.white.withValues(alpha: 0.52),
|
||||
shadowColor: const Color(0x3326BFD6),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
side: BorderSide(color: Colors.white.withValues(alpha: 0.72)),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: Colors.white.withValues(alpha: 0.64),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.75)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.7)),
|
||||
),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
indicatorColor: const Color(0x804FD6E8),
|
||||
backgroundColor: Colors.white.withValues(alpha: 0.76),
|
||||
labelTextStyle: WidgetStatePropertyAll(
|
||||
TextStyle(color: colorScheme.onSurface, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
sliderTheme: SliderThemeData(
|
||||
activeTrackColor: const Color(0xFF26BFD6),
|
||||
inactiveTrackColor: const Color(0x5526BFD6),
|
||||
thumbColor: Colors.white,
|
||||
overlayColor: const Color(0x4026BFD6),
|
||||
),
|
||||
),
|
||||
home: const MosaicHomePage(),
|
||||
);
|
||||
}
|
||||
@@ -650,28 +700,37 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
extendBody: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white.withValues(alpha: 0.45),
|
||||
title: Text(_activeSection == HomeSection.mosaic
|
||||
? 'Bottle-Cap Mosaic Prototype'
|
||||
? 'Bottle-Cap Mosaic Studio'
|
||||
: 'Cap Catalog'),
|
||||
),
|
||||
floatingActionButton: _activeSection == HomeSection.mosaic
|
||||
? FloatingActionButton.extended(
|
||||
backgroundColor: Colors.white.withValues(alpha: 0.85),
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
onPressed: _isGenerating ? null : _generate,
|
||||
icon: _isGenerating
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Icon(Icons.auto_fix_high),
|
||||
: const Icon(Icons.auto_fix_high_rounded),
|
||||
label: Text(_isGenerating ? 'Generating…' : 'Generate Mosaic'),
|
||||
)
|
||||
: null,
|
||||
body: !_isCatalogLoaded
|
||||
body: Stack(
|
||||
children: [
|
||||
const _AeroBackgroundAccents(),
|
||||
!_isCatalogLoaded
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _activeSection == HomeSection.mosaic
|
||||
? _buildMosaicScreen()
|
||||
: _buildCatalogScreen(),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: _activeSection.index,
|
||||
onDestinationSelected: (index) {
|
||||
@@ -695,12 +754,13 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
|
||||
Widget _buildMosaicScreen() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: ListView(
|
||||
children: [
|
||||
Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
_GlassCard(
|
||||
child: Wrap(
|
||||
runSpacing: 10,
|
||||
spacing: 10,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
@@ -708,10 +768,16 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
icon: const Icon(Icons.image_outlined),
|
||||
label: const Text('Import target image'),
|
||||
),
|
||||
if (_sourceImageBytes != null) const Text('Image loaded ✅'),
|
||||
if (_sourceImageBytes != null)
|
||||
const Chip(label: Text('Image loaded ✅')),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_GlassCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SegmentedButton<bool>(
|
||||
segments: const [
|
||||
ButtonSegment(value: false, label: Text('Grid W x H')),
|
||||
@@ -723,7 +789,7 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
_scheduleRegenerate();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 10),
|
||||
if (!_useCapSize)
|
||||
Row(
|
||||
children: [
|
||||
@@ -734,7 +800,7 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Grid Width')),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _gridHeightCtrl,
|
||||
@@ -751,29 +817,26 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Approx cap size in source image (pixels)'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Style Preset',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_GlassCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Style Preset', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
SegmentedButton<StylePreset>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: StylePreset.realistisch, label: Text('Realistisch')),
|
||||
ButtonSegment(
|
||||
value: StylePreset.ausgewogen, label: Text('Ausgewogen')),
|
||||
ButtonSegment(
|
||||
value: StylePreset.kuenstlerisch,
|
||||
label: Text('Künstlerisch')),
|
||||
ButtonSegment(value: StylePreset.realistisch, label: Text('Realistisch')),
|
||||
ButtonSegment(value: StylePreset.ausgewogen, label: Text('Ausgewogen')),
|
||||
ButtonSegment(value: StylePreset.kuenstlerisch, label: Text('Künstlerisch')),
|
||||
],
|
||||
selected: {_selectedPreset},
|
||||
onSelectionChanged: (s) => _applyPreset(s.first),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
_SliderRow(
|
||||
label: 'Fidelity ↔ Structure',
|
||||
leftLabel: 'Fidelity',
|
||||
@@ -813,14 +876,17 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 14),
|
||||
if (_result != null) ...[
|
||||
Text('Preview (${_result!.width} x ${_result!.height})',
|
||||
style:
|
||||
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
_GlassCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Preview (${_result!.width} x ${_result!.height})', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
RepaintBoundary(
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: RepaintBoundary(
|
||||
child: AspectRatio(
|
||||
aspectRatio: _result!.width / _result!.height,
|
||||
child: Image.memory(_result!.previewPng,
|
||||
@@ -829,9 +895,17 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
gaplessPlayback: true),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Bill of Materials',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_GlassCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Bill of Materials', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 6),
|
||||
..._result!.sortedCounts.map(
|
||||
(e) => ListTile(
|
||||
dense: true,
|
||||
@@ -841,6 +915,9 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -848,15 +925,16 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
|
||||
Widget _buildCatalogScreen() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: ListView(
|
||||
children: [
|
||||
Row(
|
||||
_GlassCard(
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
setState(() => _catalogViewMode = CatalogViewMode.list),
|
||||
icon: Icon(Icons.view_list,
|
||||
icon: Icon(Icons.view_list_rounded,
|
||||
color: _catalogViewMode == CatalogViewMode.list
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null),
|
||||
@@ -865,7 +943,7 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
setState(() => _catalogViewMode = CatalogViewMode.grid),
|
||||
icon: Icon(Icons.grid_view,
|
||||
icon: Icon(Icons.grid_view_rounded,
|
||||
color: _catalogViewMode == CatalogViewMode.grid
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null),
|
||||
@@ -873,7 +951,8 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
),
|
||||
const Spacer(),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _isCaptureFlowInProgress ? null : _captureCapPhoto,
|
||||
onPressed:
|
||||
_isCaptureFlowInProgress ? null : _captureCapPhoto,
|
||||
icon: _isCaptureFlowInProgress
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
@@ -885,11 +964,12 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
onPressed: _addCapDialog,
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
icon: const Icon(Icons.add_circle_outline_rounded),
|
||||
tooltip: 'Manuell hinzufügen'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildCatalogView(),
|
||||
],
|
||||
),
|
||||
@@ -903,7 +983,7 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
return Column(
|
||||
children: _catalog
|
||||
.map(
|
||||
(entry) => Card(
|
||||
(entry) => _GlassCard(
|
||||
child: ListTile(
|
||||
onTap: () => _editEntry(entry),
|
||||
leading: SizedBox(
|
||||
@@ -942,9 +1022,10 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final entry = _catalog[index];
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
return _GlassCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
onTap: () => _editEntry(entry),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -1163,12 +1244,16 @@ class _CapPhotoReviewPageState extends State<_CapPhotoReviewPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Deckel prüfen')),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white.withValues(alpha: 0.45),
|
||||
title: const Text('Deckel prüfen'),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(14),
|
||||
children: [
|
||||
_CircleAdjustOverlay(
|
||||
_GlassCard(
|
||||
child: _CircleAdjustOverlay(
|
||||
imageBytes: widget.imageBytes,
|
||||
imageWidth: _imageW,
|
||||
imageHeight: _imageH,
|
||||
@@ -1182,6 +1267,7 @@ class _CapPhotoReviewPageState extends State<_CapPhotoReviewPage> {
|
||||
_scheduleLiveRecalculate();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_usedFallback
|
||||
@@ -1311,6 +1397,104 @@ String _colorToHexStatic(Color color) {
|
||||
return '#${color.toARGB32().toRadixString(16).substring(2).toUpperCase()}';
|
||||
}
|
||||
|
||||
class _AeroBackgroundAccents extends StatelessWidget {
|
||||
const _AeroBackgroundAccents();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFEFFFFF), Color(0xFFE9F8FF), Color(0xFFE8FFF3)],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: -80,
|
||||
top: -90,
|
||||
child: _AccentBlob(size: 260, color: Color(0x5524D8FF)),
|
||||
),
|
||||
Positioned(
|
||||
right: -60,
|
||||
top: 120,
|
||||
child: _AccentBlob(size: 200, color: Color(0x4452E3FF)),
|
||||
),
|
||||
Positioned(
|
||||
left: 40,
|
||||
bottom: -70,
|
||||
child: _AccentBlob(size: 220, color: Color(0x4448E6AA)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AccentBlob extends StatelessWidget {
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
const _AccentBlob({required this.size, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(size),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.35),
|
||||
blurRadius: 36,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GlassCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
const _GlassCard({required this.child, this.padding = const EdgeInsets.all(12)});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.white.withValues(alpha: 0.65),
|
||||
Colors.white.withValues(alpha: 0.34),
|
||||
],
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x2226BFD6),
|
||||
blurRadius: 16,
|
||||
offset: Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(padding: padding, child: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CapThumb extends StatelessWidget {
|
||||
final CapCatalogEntry entry;
|
||||
final bool large;
|
||||
|
||||
Reference in New Issue
Block a user