diff --git a/lib/main.dart b/lib/main.dart index 563dae9..ce00b22 100644 --- a/lib/main.dart +++ b/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 @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 - ? const Center(child: CircularProgressIndicator()) - : _activeSection == HomeSection.mosaic - ? _buildMosaicScreen() - : _buildCatalogScreen(), + body: Stack( + children: [ + const _AeroBackgroundAccents(), + !_isCatalogLoaded + ? const Center(child: CircularProgressIndicator()) + : _activeSection == HomeSection.mosaic + ? _buildMosaicScreen() + : _buildCatalogScreen(), + ], + ), bottomNavigationBar: NavigationBar( selectedIndex: _activeSection.index, onDestinationSelected: (index) { @@ -695,149 +754,167 @@ class _MosaicHomePageState extends State Widget _buildMosaicScreen() { return Padding( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(14), child: ListView( children: [ - Wrap( - runSpacing: 8, - spacing: 8, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - FilledButton.icon( - onPressed: _pickImage, - icon: const Icon(Icons.image_outlined), - label: const Text('Import target image'), - ), - if (_sourceImageBytes != null) const Text('Image loaded ✅'), - ], + _GlassCard( + child: Wrap( + runSpacing: 10, + spacing: 10, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + FilledButton.icon( + onPressed: _pickImage, + icon: const Icon(Icons.image_outlined), + label: const Text('Import target image'), + ), + if (_sourceImageBytes != null) + const Chip(label: Text('Image loaded ✅')), + ], + ), ), const SizedBox(height: 12), - SegmentedButton( - segments: const [ - ButtonSegment(value: false, label: Text('Grid W x H')), - ButtonSegment(value: true, label: Text('Cap size (px)')), - ], - selected: {_useCapSize}, - onSelectionChanged: (s) { - setState(() => _useCapSize = s.first); - _scheduleRegenerate(); - }, - ), - const SizedBox(height: 8), - if (!_useCapSize) - Row( + _GlassCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: TextField( - controller: _gridWidthCtrl, - keyboardType: TextInputType.number, - decoration: - const InputDecoration(labelText: 'Grid Width')), - ), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: _gridHeightCtrl, - keyboardType: TextInputType.number, - decoration: - const InputDecoration(labelText: 'Grid Height')), + SegmentedButton( + segments: const [ + ButtonSegment(value: false, label: Text('Grid W x H')), + ButtonSegment(value: true, label: Text('Cap size (px)')), + ], + selected: {_useCapSize}, + onSelectionChanged: (s) { + setState(() => _useCapSize = s.first); + _scheduleRegenerate(); + }, ), + const SizedBox(height: 10), + if (!_useCapSize) + Row( + children: [ + Expanded( + child: TextField( + controller: _gridWidthCtrl, + keyboardType: TextInputType.number, + decoration: + const InputDecoration(labelText: 'Grid Width')), + ), + const SizedBox(width: 10), + Expanded( + child: TextField( + controller: _gridHeightCtrl, + keyboardType: TextInputType.number, + decoration: + const InputDecoration(labelText: 'Grid Height')), + ), + ], + ) + else + TextField( + controller: _capSizeCtrl, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'Approx cap size in source image (pixels)'), + ), ], - ) - else - TextField( - controller: _capSizeCtrl, - keyboardType: TextInputType.number, - 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: 8), - SegmentedButton( - segments: const [ - 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), + 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( + segments: const [ + 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), + _SliderRow( + label: 'Fidelity ↔ Structure', + leftLabel: 'Fidelity', + rightLabel: 'Structure', + value: _fidelityStructure, + onChanged: (v) { + setState(() => _fidelityStructure = v); + _onStyleChanged(); + }), + _SliderRow( + label: 'Dithering strength', + leftLabel: 'Off', + rightLabel: 'Strong', + value: _ditheringStrength, + onChanged: (v) { + setState(() => _ditheringStrength = v); + _onStyleChanged(); + }), + _SliderRow( + label: 'Edge emphasis', + leftLabel: 'Soft', + rightLabel: 'Crisp', + value: _edgeEmphasis, + onChanged: (v) { + setState(() => _edgeEmphasis = v); + _onStyleChanged(); + }), + _SliderRow( + label: 'Color tolerance / variation', + leftLabel: 'Strict', + rightLabel: 'Varied', + value: _colorVariation, + onChanged: (v) { + setState(() => _colorVariation = v); + _onStyleChanged(); + }), + ], + ), + ), + const SizedBox(height: 14), + if (_result != null) ...[ + _GlassCard( child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _SliderRow( - label: 'Fidelity ↔ Structure', - leftLabel: 'Fidelity', - rightLabel: 'Structure', - value: _fidelityStructure, - onChanged: (v) { - setState(() => _fidelityStructure = v); - _onStyleChanged(); - }), - _SliderRow( - label: 'Dithering strength', - leftLabel: 'Off', - rightLabel: 'Strong', - value: _ditheringStrength, - onChanged: (v) { - setState(() => _ditheringStrength = v); - _onStyleChanged(); - }), - _SliderRow( - label: 'Edge emphasis', - leftLabel: 'Soft', - rightLabel: 'Crisp', - value: _edgeEmphasis, - onChanged: (v) { - setState(() => _edgeEmphasis = v); - _onStyleChanged(); - }), - _SliderRow( - label: 'Color tolerance / variation', - leftLabel: 'Strict', - rightLabel: 'Varied', - value: _colorVariation, - onChanged: (v) { - setState(() => _colorVariation = v); - _onStyleChanged(); - }), + Text('Preview (${_result!.width} x ${_result!.height})', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(18), + child: RepaintBoundary( + child: AspectRatio( + aspectRatio: _result!.width / _result!.height, + child: Image.memory(_result!.previewPng, + fit: BoxFit.fill, + filterQuality: FilterQuality.none, + gaplessPlayback: true), + ), + ), + ), ], ), ), - ), - const SizedBox(height: 16), - if (_result != null) ...[ - Text('Preview (${_result!.width} x ${_result!.height})', - style: - const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - RepaintBoundary( - child: AspectRatio( - aspectRatio: _result!.width / _result!.height, - child: Image.memory(_result!.previewPng, - fit: BoxFit.fill, - filterQuality: FilterQuality.none, - gaplessPlayback: true), - ), - ), - const SizedBox(height: 16), - const Text('Bill of Materials', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - ..._result!.sortedCounts.map( - (e) => ListTile( - dense: true, - leading: CircleAvatar(backgroundColor: e.key.color), - title: Text(e.key.name), - trailing: Text('${e.value} caps'), + 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, + leading: CircleAvatar(backgroundColor: e.key.color), + title: Text(e.key.name), + trailing: Text('${e.value} caps'), + ), + ), + ], ), ), ], @@ -848,48 +925,51 @@ class _MosaicHomePageState extends State Widget _buildCatalogScreen() { return Padding( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(14), child: ListView( children: [ - Row( - children: [ - IconButton( - onPressed: () => - setState(() => _catalogViewMode = CatalogViewMode.list), - icon: Icon(Icons.view_list, - color: _catalogViewMode == CatalogViewMode.list - ? Theme.of(context).colorScheme.primary - : null), - tooltip: 'Listenansicht', - ), - IconButton( - onPressed: () => - setState(() => _catalogViewMode = CatalogViewMode.grid), - icon: Icon(Icons.grid_view, - color: _catalogViewMode == CatalogViewMode.grid - ? Theme.of(context).colorScheme.primary - : null), - tooltip: 'Rasteransicht', - ), - const Spacer(), - OutlinedButton.icon( - onPressed: _isCaptureFlowInProgress ? null : _captureCapPhoto, - icon: _isCaptureFlowInProgress - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.photo_camera_outlined), - label: Text(_isCaptureFlowInProgress ? 'Läuft…' : 'Foto')), - const SizedBox(width: 8), - IconButton( - onPressed: _addCapDialog, - icon: const Icon(Icons.add_circle_outline), - tooltip: 'Manuell hinzufügen'), - ], + _GlassCard( + child: Row( + children: [ + IconButton( + onPressed: () => + setState(() => _catalogViewMode = CatalogViewMode.list), + icon: Icon(Icons.view_list_rounded, + color: _catalogViewMode == CatalogViewMode.list + ? Theme.of(context).colorScheme.primary + : null), + tooltip: 'Listenansicht', + ), + IconButton( + onPressed: () => + setState(() => _catalogViewMode = CatalogViewMode.grid), + icon: Icon(Icons.grid_view_rounded, + color: _catalogViewMode == CatalogViewMode.grid + ? Theme.of(context).colorScheme.primary + : null), + tooltip: 'Rasteransicht', + ), + const Spacer(), + OutlinedButton.icon( + onPressed: + _isCaptureFlowInProgress ? null : _captureCapPhoto, + icon: _isCaptureFlowInProgress + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.photo_camera_outlined), + label: Text(_isCaptureFlowInProgress ? 'Läuft…' : 'Foto')), + const SizedBox(width: 8), + IconButton( + onPressed: _addCapDialog, + 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 return Column( children: _catalog .map( - (entry) => Card( + (entry) => _GlassCard( child: ListTile( onTap: () => _editEntry(entry), leading: SizedBox( @@ -942,9 +1022,10 @@ class _MosaicHomePageState extends State ), 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,24 +1244,29 @@ 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( - imageBytes: widget.imageBytes, - imageWidth: _imageW, - imageHeight: _imageH, - circleX: _circleX, - circleY: _circleY, - circleR: _circleR, - onCircleChanged: (x, y, r) { - setState(() { - _setAndClampCircle(x: x, y: y, r: r); - }); - _scheduleLiveRecalculate(); - }, + _GlassCard( + child: _CircleAdjustOverlay( + imageBytes: widget.imageBytes, + imageWidth: _imageW, + imageHeight: _imageH, + circleX: _circleX, + circleY: _circleY, + circleR: _circleR, + onCircleChanged: (x, y, r) { + setState(() { + _setAndClampCircle(x: x, y: y, r: r); + }); + _scheduleLiveRecalculate(); + }, + ), ), const SizedBox(height: 8), Text( @@ -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;