From 814705cac66100b9302dbfac20257a9a61a595e8 Mon Sep 17 00:00:00 2001 From: gary Date: Mon, 23 Feb 2026 08:25:06 +0100 Subject: [PATCH] feat(projects): add Projects tab with load/delete list --- lib/main.dart | 130 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 20 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index e10f811..8939602 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -84,7 +84,7 @@ enum CatalogViewMode { list, grid } enum ColorExtractionMode { dominant, average } -enum HomeSection { mosaic, catalog } +enum HomeSection { mosaic, catalog, projects } class MosaicHomePage extends StatefulWidget { const MosaicHomePage({super.key}); @@ -116,6 +116,7 @@ class _MosaicHomePageState extends State bool _isCaptureFlowInProgress = false; bool _isRecoveringCapture = false; bool _isProjectBusy = false; + List _projectFiles = []; double _fidelityStructure = 0.5; double _ditheringStrength = 0.35; @@ -133,6 +134,7 @@ class _MosaicHomePageState extends State _gridHeightCtrl.addListener(_scheduleRegenerate); _capSizeCtrl.addListener(_scheduleRegenerate); _loadCatalog(); + _refreshProjectFiles(); WidgetsBinding.instance.addPostFrameCallback((_) { _recoverCaptureOnResumeOrStart(); _loadProject(silent: true); @@ -177,16 +179,42 @@ class _MosaicHomePageState extends State return file.path; } - Future _projectFile() async { + Future _projectsDir() async { final docs = await getApplicationDocumentsDirectory(); - return File('${docs.path}/mosaic_project.json'); + final dir = Directory('${docs.path}/projects'); + await dir.create(recursive: true); + return dir; + } + + String _projectFilename() { + final now = DateTime.now(); + final stamp = now + .toIso8601String() + .replaceAll(':', '-') + .replaceAll('.', '-') + .replaceAll('T', '_'); + return 'project_$stamp.json'; + } + + Future _refreshProjectFiles() async { + try { + final dir = await _projectsDir(); + final files = dir + .listSync() + .whereType() + .where((f) => f.path.endsWith('.json')) + .toList() + ..sort((a, b) => b.path.compareTo(a.path)); + if (mounted) setState(() => _projectFiles = files); + } catch (_) {} } Future _saveProject({bool silent = false}) async { if (_isProjectBusy) return; _isProjectBusy = true; try { - final file = await _projectFile(); + final projectsDir = await _projectsDir(); + final file = File('${projectsDir.path}/${_projectFilename()}'); final payload = { 'useCapSize': _useCapSize, 'gridWidth': _gridWidthCtrl.text, @@ -197,11 +225,11 @@ class _MosaicHomePageState extends State 'edgeEmphasis': _edgeEmphasis, 'colorVariation': _colorVariation, 'selectedPreset': _selectedPreset.name, - 'activeSection': _activeSection.name, 'sourceImageBase64': _sourceImageBytes == null ? null : base64Encode(_sourceImageBytes!), }; await file.writeAsString(jsonEncode(payload), flush: true); + await _refreshProjectFiles(); if (!silent && mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Projekt gespeichert ✅')), @@ -218,15 +246,19 @@ class _MosaicHomePageState extends State } } - Future _loadProject({bool silent = false}) async { + Future _loadProject({bool silent = false, File? fromFile}) async { if (_isProjectBusy) return; _isProjectBusy = true; try { - final file = await _projectFile(); + File? file = fromFile; + if (file == null) { + await _refreshProjectFiles(); + if (_projectFiles.isEmpty) return; + file = _projectFiles.first; + } if (!await file.exists()) return; - final data = - jsonDecode(await file.readAsString()) as Map; + final data = jsonDecode(await file.readAsString()) as Map; final sourceB64 = data['sourceImageBase64'] as String?; final source = sourceB64 == null ? null : base64Decode(sourceB64); @@ -253,21 +285,14 @@ class _MosaicHomePageState extends State orElse: () => _selectedPreset, ); - final sectionName = data['activeSection'] as String?; - _activeSection = HomeSection.values.firstWhere( - (s) => s.name == sectionName, - orElse: () => _activeSection, - ); - if (source != null) { _sourceImageBytes = source; _result = null; } + _activeSection = HomeSection.mosaic; }); - if (source != null) { - await _generate(); - } + if (source != null) await _generate(); if (!silent && mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Projekt geladen ✅')), @@ -284,6 +309,24 @@ class _MosaicHomePageState extends State } } + Future _deleteProject(File file) async { + try { + await file.delete(); + await _refreshProjectFiles(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Projekt gelöscht')), + ); + } + } catch (_) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Löschen fehlgeschlagen')), + ); + } + } + } + Future _loadCatalog() async { final defaults = [ CapCatalogEntry.newEntry(name: 'White', color: const Color(0xFFF2F2F2)), @@ -824,7 +867,9 @@ class _MosaicHomePageState extends State backgroundColor: Colors.white.withValues(alpha: 0.45), title: Text(_activeSection == HomeSection.mosaic ? 'Bottle-Cap Mosaic Studio' - : 'Cap Catalog'), + : _activeSection == HomeSection.catalog + ? 'Cap Catalog' + : 'Projekte'), ), floatingActionButton: _activeSection == HomeSection.mosaic ? FloatingActionButton.extended( @@ -847,7 +892,9 @@ class _MosaicHomePageState extends State ? const Center(child: CircularProgressIndicator()) : _activeSection == HomeSection.mosaic ? _buildMosaicScreen() - : _buildCatalogScreen(), + : _activeSection == HomeSection.catalog + ? _buildCatalogScreen() + : _buildProjectsScreen(), ], ), bottomNavigationBar: NavigationBar( @@ -866,11 +913,54 @@ class _MosaicHomePageState extends State selectedIcon: Icon(Icons.inventory_2), label: 'Catalog', ), + NavigationDestination( + icon: Icon(Icons.folder_copy_outlined), + selectedIcon: Icon(Icons.folder_copy), + label: 'Projekte', + ), ], ), ); } + Widget _buildProjectsScreen() { + if (_projectFiles.isEmpty) { + return const Center(child: Text('Noch keine gespeicherten Projekte.')); + } + + return ListView.builder( + padding: const EdgeInsets.all(14), + itemCount: _projectFiles.length, + itemBuilder: (context, index) { + final file = _projectFiles[index]; + final name = file.uri.pathSegments.isEmpty + ? file.path + : file.uri.pathSegments.last; + return Card( + child: ListTile( + leading: const Icon(Icons.insert_drive_file_outlined), + title: Text(name), + trailing: Wrap( + spacing: 8, + children: [ + IconButton( + tooltip: 'Laden', + icon: const Icon(Icons.playlist_add_check_circle_outlined), + onPressed: () => _loadProject(fromFile: file), + ), + IconButton( + tooltip: 'Löschen', + icon: const Icon(Icons.delete_outline), + onPressed: () => _deleteProject(file), + ), + ], + ), + ), + ); + }, + ); + } + Widget _buildMosaicScreen() { return Padding( padding: const EdgeInsets.all(14),