Compare commits
2 Commits
build-2026
...
apk-overfl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84d649ac6d | ||
|
|
75a3a8dd60 |
288
lib/main.dart
288
lib/main.dart
@@ -19,9 +19,59 @@ class KorkenMosaicApp extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
const seed = Color(0xFF26BFD6);
|
||||||
|
final colorScheme = ColorScheme.fromSeed(
|
||||||
|
seedColor: seed,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
);
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Korken Mosaic',
|
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(),
|
home: const MosaicHomePage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -650,28 +700,37 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
extendBody: true,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.white.withValues(alpha: 0.45),
|
||||||
title: Text(_activeSection == HomeSection.mosaic
|
title: Text(_activeSection == HomeSection.mosaic
|
||||||
? 'Bottle-Cap Mosaic Prototype'
|
? 'Bottle-Cap Mosaic Studio'
|
||||||
: 'Cap Catalog'),
|
: 'Cap Catalog'),
|
||||||
),
|
),
|
||||||
floatingActionButton: _activeSection == HomeSection.mosaic
|
floatingActionButton: _activeSection == HomeSection.mosaic
|
||||||
? FloatingActionButton.extended(
|
? FloatingActionButton.extended(
|
||||||
|
backgroundColor: Colors.white.withValues(alpha: 0.85),
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||||
onPressed: _isGenerating ? null : _generate,
|
onPressed: _isGenerating ? null : _generate,
|
||||||
icon: _isGenerating
|
icon: _isGenerating
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 18,
|
width: 18,
|
||||||
height: 18,
|
height: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2))
|
child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
: const Icon(Icons.auto_fix_high),
|
: const Icon(Icons.auto_fix_high_rounded),
|
||||||
label: Text(_isGenerating ? 'Generating…' : 'Generate Mosaic'),
|
label: Text(_isGenerating ? 'Generating…' : 'Generate Mosaic'),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
body: !_isCatalogLoaded
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
const _AeroBackgroundAccents(),
|
||||||
|
!_isCatalogLoaded
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: _activeSection == HomeSection.mosaic
|
: _activeSection == HomeSection.mosaic
|
||||||
? _buildMosaicScreen()
|
? _buildMosaicScreen()
|
||||||
: _buildCatalogScreen(),
|
: _buildCatalogScreen(),
|
||||||
|
],
|
||||||
|
),
|
||||||
bottomNavigationBar: NavigationBar(
|
bottomNavigationBar: NavigationBar(
|
||||||
selectedIndex: _activeSection.index,
|
selectedIndex: _activeSection.index,
|
||||||
onDestinationSelected: (index) {
|
onDestinationSelected: (index) {
|
||||||
@@ -695,12 +754,13 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
|
|
||||||
Widget _buildMosaicScreen() {
|
Widget _buildMosaicScreen() {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(14),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
Wrap(
|
_GlassCard(
|
||||||
runSpacing: 8,
|
child: Wrap(
|
||||||
spacing: 8,
|
runSpacing: 10,
|
||||||
|
spacing: 10,
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
@@ -708,10 +768,16 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
icon: const Icon(Icons.image_outlined),
|
icon: const Icon(Icons.image_outlined),
|
||||||
label: const Text('Import target image'),
|
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),
|
const SizedBox(height: 12),
|
||||||
|
_GlassCard(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
SegmentedButton<bool>(
|
SegmentedButton<bool>(
|
||||||
segments: const [
|
segments: const [
|
||||||
ButtonSegment(value: false, label: Text('Grid W x H')),
|
ButtonSegment(value: false, label: Text('Grid W x H')),
|
||||||
@@ -723,7 +789,7 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
_scheduleRegenerate();
|
_scheduleRegenerate();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 10),
|
||||||
if (!_useCapSize)
|
if (!_useCapSize)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -734,7 +800,7 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
decoration:
|
decoration:
|
||||||
const InputDecoration(labelText: 'Grid Width')),
|
const InputDecoration(labelText: 'Grid Width')),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _gridHeightCtrl,
|
controller: _gridHeightCtrl,
|
||||||
@@ -751,29 +817,26 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Approx cap size in source image (pixels)'),
|
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),
|
const SizedBox(height: 8),
|
||||||
SegmentedButton<StylePreset>(
|
SegmentedButton<StylePreset>(
|
||||||
segments: const [
|
segments: const [
|
||||||
ButtonSegment(
|
ButtonSegment(value: StylePreset.realistisch, label: Text('Realistisch')),
|
||||||
value: StylePreset.realistisch, label: Text('Realistisch')),
|
ButtonSegment(value: StylePreset.ausgewogen, label: Text('Ausgewogen')),
|
||||||
ButtonSegment(
|
ButtonSegment(value: StylePreset.kuenstlerisch, label: Text('Künstlerisch')),
|
||||||
value: StylePreset.ausgewogen, label: Text('Ausgewogen')),
|
|
||||||
ButtonSegment(
|
|
||||||
value: StylePreset.kuenstlerisch,
|
|
||||||
label: Text('Künstlerisch')),
|
|
||||||
],
|
],
|
||||||
selected: {_selectedPreset},
|
selected: {_selectedPreset},
|
||||||
onSelectionChanged: (s) => _applyPreset(s.first),
|
onSelectionChanged: (s) => _applyPreset(s.first),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_SliderRow(
|
_SliderRow(
|
||||||
label: 'Fidelity ↔ Structure',
|
label: 'Fidelity ↔ Structure',
|
||||||
leftLabel: 'Fidelity',
|
leftLabel: 'Fidelity',
|
||||||
@@ -813,14 +876,17 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 14),
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (_result != null) ...[
|
if (_result != null) ...[
|
||||||
Text('Preview (${_result!.width} x ${_result!.height})',
|
_GlassCard(
|
||||||
style:
|
child: Column(
|
||||||
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Preview (${_result!.width} x ${_result!.height})', style: Theme.of(context).textTheme.titleMedium),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
RepaintBoundary(
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
child: RepaintBoundary(
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: _result!.width / _result!.height,
|
aspectRatio: _result!.width / _result!.height,
|
||||||
child: Image.memory(_result!.previewPng,
|
child: Image.memory(_result!.previewPng,
|
||||||
@@ -829,9 +895,17 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
gaplessPlayback: true),
|
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(
|
..._result!.sortedCounts.map(
|
||||||
(e) => ListTile(
|
(e) => ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
@@ -841,6 +915,9 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -848,15 +925,16 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
|
|
||||||
Widget _buildCatalogScreen() {
|
Widget _buildCatalogScreen() {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(14),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
_GlassCard(
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
setState(() => _catalogViewMode = CatalogViewMode.list),
|
setState(() => _catalogViewMode = CatalogViewMode.list),
|
||||||
icon: Icon(Icons.view_list,
|
icon: Icon(Icons.view_list_rounded,
|
||||||
color: _catalogViewMode == CatalogViewMode.list
|
color: _catalogViewMode == CatalogViewMode.list
|
||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
: null),
|
: null),
|
||||||
@@ -865,7 +943,7 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
setState(() => _catalogViewMode = CatalogViewMode.grid),
|
setState(() => _catalogViewMode = CatalogViewMode.grid),
|
||||||
icon: Icon(Icons.grid_view,
|
icon: Icon(Icons.grid_view_rounded,
|
||||||
color: _catalogViewMode == CatalogViewMode.grid
|
color: _catalogViewMode == CatalogViewMode.grid
|
||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
: null),
|
: null),
|
||||||
@@ -873,7 +951,8 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: _isCaptureFlowInProgress ? null : _captureCapPhoto,
|
onPressed:
|
||||||
|
_isCaptureFlowInProgress ? null : _captureCapPhoto,
|
||||||
icon: _isCaptureFlowInProgress
|
icon: _isCaptureFlowInProgress
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
@@ -885,11 +964,12 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: _addCapDialog,
|
onPressed: _addCapDialog,
|
||||||
icon: const Icon(Icons.add_circle_outline),
|
icon: const Icon(Icons.add_circle_outline_rounded),
|
||||||
tooltip: 'Manuell hinzufügen'),
|
tooltip: 'Manuell hinzufügen'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
_buildCatalogView(),
|
_buildCatalogView(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -903,7 +983,7 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
return Column(
|
return Column(
|
||||||
children: _catalog
|
children: _catalog
|
||||||
.map(
|
.map(
|
||||||
(entry) => Card(
|
(entry) => _GlassCard(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
onTap: () => _editEntry(entry),
|
onTap: () => _editEntry(entry),
|
||||||
leading: SizedBox(
|
leading: SizedBox(
|
||||||
@@ -936,15 +1016,17 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
itemCount: _catalog.length,
|
itemCount: _catalog.length,
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 2,
|
crossAxisCount: 2,
|
||||||
childAspectRatio: 1.2,
|
// Mehr vertikaler Platz pro Card, damit Name/Hex/Delete nicht überlaufen.
|
||||||
|
childAspectRatio: 0.92,
|
||||||
mainAxisSpacing: 8,
|
mainAxisSpacing: 8,
|
||||||
crossAxisSpacing: 8,
|
crossAxisSpacing: 8,
|
||||||
),
|
),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final entry = _catalog[index];
|
final entry = _catalog[index];
|
||||||
return Card(
|
return _GlassCard(
|
||||||
clipBehavior: Clip.antiAlias,
|
padding: EdgeInsets.zero,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(22),
|
||||||
onTap: () => _editEntry(entry),
|
onTap: () => _editEntry(entry),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
@@ -970,10 +1052,13 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints:
|
||||||
|
const BoxConstraints.tightFor(width: 30, height: 30),
|
||||||
onPressed: _catalog.length <= 1
|
onPressed: _catalog.length <= 1
|
||||||
? null
|
? null
|
||||||
: () => _deleteEntry(entry),
|
: () => _deleteEntry(entry),
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline, size: 20),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1163,12 +1248,16 @@ class _CapPhotoReviewPageState extends State<_CapPhotoReviewPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
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(
|
body: SafeArea(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(14),
|
||||||
children: [
|
children: [
|
||||||
_CircleAdjustOverlay(
|
_GlassCard(
|
||||||
|
child: _CircleAdjustOverlay(
|
||||||
imageBytes: widget.imageBytes,
|
imageBytes: widget.imageBytes,
|
||||||
imageWidth: _imageW,
|
imageWidth: _imageW,
|
||||||
imageHeight: _imageH,
|
imageHeight: _imageH,
|
||||||
@@ -1182,6 +1271,7 @@ class _CapPhotoReviewPageState extends State<_CapPhotoReviewPage> {
|
|||||||
_scheduleLiveRecalculate();
|
_scheduleLiveRecalculate();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
_usedFallback
|
_usedFallback
|
||||||
@@ -1311,6 +1401,104 @@ String _colorToHexStatic(Color color) {
|
|||||||
return '#${color.toARGB32().toRadixString(16).substring(2).toUpperCase()}';
|
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 {
|
class _CapThumb extends StatelessWidget {
|
||||||
final CapCatalogEntry entry;
|
final CapCatalogEntry entry;
|
||||||
final bool large;
|
final bool large;
|
||||||
|
|||||||
Reference in New Issue
Block a user