commit ccfa0612d95167c7025dd06d9f38d81f3712ccf5
parent fafebf83417b68babfa39ec6002fb227ddfcf32a
Author: Nilotpal Gupta <nilotpalgupta0701@gmail.com>
Date: Mon, 1 Dec 2025 07:53:57 +0530
flask service + asset generation + sketch to image + ui integration of assets
Diffstat:
8 files changed, 262 insertions(+), 46 deletions(-)
diff --git a/lib/data/database.dart b/lib/data/database.dart
@@ -1,12 +1,6 @@
-import 'dart:convert';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
-import 'models/project_model.dart';
-import 'models/image_model.dart';
-import 'models/file_model.dart';
-import 'models/note_model.dart';
-
class AppDatabase {
static Database? _db;
static const String _dbName = 'database_v6.db';
@@ -36,6 +30,7 @@ class AppDatabase {
parent_id INTEGER,
global_stylesheet TEXT,
last_accessed_at TEXT,
+ assets_path TEXT DEFAULT '[]',
created_at TEXT,
FOREIGN KEY (parent_id) REFERENCES projects (id) ON DELETE CASCADE
)
diff --git a/lib/data/models/project_model.dart b/lib/data/models/project_model.dart
@@ -8,6 +8,7 @@ class ProjectModel {
final String? globalStylesheet; // Stored as JSON string
final DateTime lastAccessedAt;
final DateTime createdAt;
+ final List<String> assetsPath;
ProjectModel({
this.id,
@@ -17,6 +18,7 @@ class ProjectModel {
this.globalStylesheet,
required this.lastAccessedAt,
required this.createdAt,
+ this.assetsPath = const [],
});
bool get isEvent => parentId != null;
@@ -40,6 +42,7 @@ class ProjectModel {
'global_stylesheet': globalStylesheet,
'last_accessed_at': lastAccessedAt.toIso8601String(),
'created_at': createdAt.toIso8601String(),
+ 'assets_path': jsonEncode(assetsPath),
};
}
@@ -52,6 +55,8 @@ class ProjectModel {
globalStylesheet: map['global_stylesheet'],
lastAccessedAt: DateTime.parse(map['last_accessed_at']),
createdAt: DateTime.parse(map['created_at']),
+ assetsPath:
+ map['assets_path'] != null ? List<String>.from(jsonDecode(map['assets_path'])) : [],
);
}
}
diff --git a/lib/data/repos/file_repo.dart b/lib/data/repos/file_repo.dart
@@ -70,6 +70,18 @@ class FileRepo {
return res.map((e) => e['file_path'] as String).toList();
}
+ Future<FileModel?> getByFilePath(String path) async {
+ final db = await AppDatabase.db;
+ final res = await db.query(
+ 'files',
+ where: 'file_path = ?',
+ whereArgs: [path],
+ limit: 1,
+ );
+ if (res.isNotEmpty) return FileModel.fromMap(res.first);
+ return null;
+ }
+
Future<List<FileModel>> getRecentFiles({int limit = 10}) async {
final db = await AppDatabase.db;
final res = await db.query(
diff --git a/lib/data/repos/note_repo.dart b/lib/data/repos/note_repo.dart
@@ -2,8 +2,6 @@ import '../database.dart';
import '../models/note_model.dart';
class NoteRepo {
- final _db = AppDatabase();
-
Future<int> addNote(NoteModel note) async {
final db = await AppDatabase.db;
return await db.insert('notes', note.toMap());
diff --git a/lib/data/repos/project_repo.dart b/lib/data/repos/project_repo.dart
@@ -1,3 +1,5 @@
+import 'dart:convert';
+
import '../database.dart';
import '../models/project_model.dart';
@@ -117,4 +119,18 @@ class ProjectRepo {
final db = await AppDatabase.db;
await db.delete('projects', where: 'id = ?', whereArgs: [id]);
}
+
+ Future<void> updateAssets(int projectId, List<String> assets) async {
+ final db = await AppDatabase.db;
+
+ await db.update(
+ 'projects',
+ {
+ 'assets_path': jsonEncode(assets),
+ 'last_accessed_at': DateTime.now().toIso8601String(),
+ },
+ where: 'id = ?',
+ whereArgs: [projectId],
+ );
+ }
}
diff --git a/lib/services/analyze/image_analyzer.dart b/lib/services/analyze/image_analyzer.dart
@@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:isolate';
import 'dart:ui';
import 'dart:developer' as dev;
+import 'package:adobe/services/flask_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
@@ -89,6 +90,7 @@ class ImageAnalyzerService {
result = await task(imagePath, assetPaths);
}
+ debugPrint("📱: ${result.toString()}");
stopwatch.stop();
timelineTask.finish();
result['execution_time'] = stopwatch.elapsedMilliseconds;
@@ -154,8 +156,6 @@ class ImageAnalyzerService {
final results = await Future.wait([
// --- GROUP A: PARALLEL ---
- // TODO: 'Subject'
-
// 1. Layout
shouldRun(['Compositions'])
? _runProfiledJob(
@@ -307,6 +307,25 @@ class ImageAnalyzerService {
},
)
: skipTask(),
+
+ // --- GROUP C: SERVER SIDE ---
+
+ // 9. Subject
+ shouldRun(['Subject', 'Asset'])
+ ? _runProfiledJob(
+ name: 'Subject',
+ imagePath: imagePath,
+ rootToken: token,
+ runInIsolate: false,
+ assetPaths: assetPaths,
+ task: (path, _) async {
+ final String? location = await FlaskService().generateAsset(imagePath: path);
+ final res = {"success": true, "scores": {'image': location}, "error": null};
+ return res;
+ },
+ )
+ : skipTask(),
+
]);
totalSw.stop();
@@ -325,6 +344,7 @@ class ImageAnalyzerService {
'Lighting': results[5]['execution_time'],
'Era': results[6]['execution_time'],
'Font': results[7]['execution_time'],
+ 'Subject': results[8]['execution_time'],
},
};
_logSummary(logResult);
@@ -342,6 +362,7 @@ class ImageAnalyzerService {
'Era': {"scores": results[6]['scores']},
'Layout': {"scores": results[0]['scores']},
'Font': {"scores": results[7]['scores']},
+ 'Subject': {"scores": results[8]['scores']},
},
},
'error': null,
diff --git a/lib/services/flask_service.dart b/lib/services/flask_service.dart
@@ -1,7 +1,11 @@
import 'dart:convert';
import 'dart:io';
+import 'package:adobe/data/repos/file_repo.dart';
+import 'package:adobe/data/repos/image_repo.dart';
+import 'package:adobe/data/repos/project_repo.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
+import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
class FlaskService {
@@ -9,7 +13,11 @@ class FlaskService {
// CONFIGURATION
// ===========================================================================
- static const String _serverUrl = 'https://locustlike-trieciously-rudolph.ngrok-free.dev';
+ // NOTE: REPLACE WITH YOUR WIFI IP ADDRESS
+ // BOTH PC AND MOBILE SHOULD BE ON SAME WIFI
+ // NO NEED FOR NGORK OR SMEE
+ // PORT: 5000, http
+ static const String _serverUrl = 'http://172.16.114.193:5000'; // --> READ NOTE (REPLACE WITH IITG_CONNECT WIFI IP)
static const Map<String, String> _headers = {'Content-Type': 'application/json'};
// ===========================================================================
@@ -93,12 +101,39 @@ class FlaskService {
final String? base64Image = await _encodeFile(imagePath);
if (base64Image == null) return null;
- return _performImageOperation(
- endpoint: '/remove-background',
+ final String? path = await _performImageOperation(
+ endpoint: '/asset',
logPrefix: '✂️ Asset Gen',
body: {'image': base64Image},
filenamePrefix: 'asset',
);
+
+ if (path != null) {
+ // 1. Find the project ID
+ int? projectId;
+
+ final imagemodel = await ImageRepo().getByFilePath(imagePath);
+ if (imagemodel != null) {
+ projectId = imagemodel.projectId;
+ } else {
+ final filemodel = await FileRepo().getByFilePath(imagePath);
+ if (filemodel != null) {
+ projectId = filemodel.projectId;
+ }
+ }
+
+ // 2. Update the Project Repo AND SAVE TO DATABASE
+ if (projectId != null) {
+ final project = await ProjectRepo().getProjectById(projectId);
+ if (project != null) {
+ project.assetsPath.add(path); // Update memory
+ await ProjectRepo().updateAssets(projectId, project.assetsPath);
+ debugPrint("✅ Asset path saved to Project DB: $path");
+ }
+ }
+ }
+
+ return path;
}
// ===========================================================================
@@ -190,13 +225,18 @@ class FlaskService {
final Uint8List imageBytes = base64Decode(data['image']);
final directory = await getApplicationDocumentsDirectory();
- final imagesDir = Directory('${directory.path}/generated_images');
+ // Use join for safe path construction
+ final imagesDirPath = p.join(directory.path, 'generated_images');
+ final imagesDir = Directory(imagesDirPath);
+
if (!await imagesDir.exists()) await imagesDir.create(recursive: true);
final timestamp = DateTime.now().millisecondsSinceEpoch;
final safePrefix = prefix.replaceAll(RegExp(r'[^\w\s]'), '').trim().replaceAll(' ', '_');
final shortPrefix = safePrefix.length > 20 ? safePrefix.substring(0, 20) : safePrefix;
- final String filePath = '${imagesDir.path}/${shortPrefix}_$timestamp.png';
+
+ // Use join here too
+ final String filePath = p.join(imagesDir.path, '${shortPrefix}_$timestamp.png');
await File(filePath).writeAsBytes(imageBytes);
debugPrint("✅ Image saved: $filePath");
diff --git a/lib/ui/pages/stylesheet_page.dart b/lib/ui/pages/stylesheet_page.dart
@@ -1,4 +1,5 @@
import 'dart:convert';
+import 'dart:io';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:adobe/ui/styles/variables.dart';
@@ -8,6 +9,8 @@ import 'package:adobe/data/repos/image_repo.dart';
import 'package:adobe/data/repos/project_repo.dart';
import 'package:adobe/services/python_service.dart';
import 'package:adobe/data/repos/note_repo.dart';
+import 'package:path/path.dart' as p;
+import 'package:path_provider/path_provider.dart';
class StylesheetPage extends StatefulWidget {
final int projectId;
@@ -27,6 +30,9 @@ class _StylesheetPageState extends State<StylesheetPage> {
Map<String, dynamic>? _stylesheetMap;
String? _rawJsonString;
+
+ // New state variable to hold assets from ProjectRepo
+ List<String> _projectAssets = [];
// Cache for font name lookups
final Map<String, String> _fontNameCache = {};
@@ -79,17 +85,51 @@ class _StylesheetPageState extends State<StylesheetPage> {
return dirtyName;
}
+ Future<File?> _resolveFile(String path) async {
+ // 1. Try the path exactly as saved
+ final file = File(path);
+ if (await file.exists()) return file;
+
+ // 2. If that fails (iOS UUID change?), try to find it in the current docs dir
+ try {
+ final filename = p.basename(path); // Get "image_123.png" from the long path
+ final dir = await getApplicationDocumentsDirectory();
+
+ // Reconstruct path: CurrentDir + generated_images + filename
+ final fixedPath = p.join(dir.path, 'generated_images', filename);
+
+ final fixedFile = File(fixedPath);
+ if (await fixedFile.exists()) {
+ return fixedFile;
+ }
+ } catch (e) {
+ debugPrint("Error resolving file path: $e");
+ }
+
+ return null;
+ }
+
Future<void> _loadSavedStylesheet() async {
final project = await ProjectRepo().getProjectById(_currentProjectId);
- if (project?.globalStylesheet != null && project!.globalStylesheet!.isNotEmpty) {
- String raw = project.globalStylesheet!;
+
+ if (project == null) return;
+
+ // 1. Load Assets directly from Project Model
+ List<String> currentAssets = project.assetsPath;
+
+ // 2. Load Stylesheet JSON
+ Map<String, dynamic>? parsedMap;
+ String? rawJson;
+
+ if (project.globalStylesheet != null && project.globalStylesheet!.isNotEmpty) {
+ rawJson = project.globalStylesheet!;
dynamic parsed;
try {
- parsed = jsonDecode(raw);
+ parsed = jsonDecode(rawJson);
} catch (e) {
try {
- parsed = jsonDecode(_cleanJsonString(raw));
+ parsed = jsonDecode(_cleanJsonString(rawJson));
} catch (_) {}
}
@@ -97,21 +137,22 @@ class _StylesheetPageState extends State<StylesheetPage> {
try { parsed = jsonDecode(parsed); } catch (_) {}
}
- if (mounted) {
- setState(() {
- _rawJsonString = raw;
- if (parsed is Map<String, dynamic>) {
- if (parsed.containsKey('results') && parsed['results'] is Map) {
- _stylesheetMap = parsed['results'];
- } else {
- _stylesheetMap = parsed;
- }
- } else {
- _stylesheetMap = null;
- }
- });
+ if (parsed is Map<String, dynamic>) {
+ if (parsed.containsKey('results') && parsed['results'] is Map) {
+ parsedMap = parsed['results'];
+ } else {
+ parsedMap = parsed;
+ }
}
}
+
+ if (mounted) {
+ setState(() {
+ _projectAssets = currentAssets;
+ _rawJsonString = rawJson;
+ _stylesheetMap = parsedMap;
+ });
+ }
}
Future<void> _generateStylesheet() async {
@@ -156,14 +197,8 @@ class _StylesheetPageState extends State<StylesheetPage> {
final jsonString = jsonEncode(result);
await ProjectRepo().updateStylesheet(_currentProjectId, jsonString);
- setState(() {
- if (result.containsKey('results') && result['results'] is Map) {
- _stylesheetMap = result['results'];
- } else {
- _stylesheetMap = result;
- }
- _rawJsonString = jsonString;
- });
+ // Reload everything (assets + stylesheet) to keep sync
+ await _loadSavedStylesheet();
}
} catch (e) {
debugPrint("Gen Error: $e");
@@ -193,12 +228,13 @@ class _StylesheetPageState extends State<StylesheetPage> {
onProjectChanged: (p) => setState(() {
_currentProjectId = p.id!;
_stylesheetMap = null;
+ _projectAssets = [];
_loadSavedStylesheet();
}),
),
body: _isLoading
? const Center(child: CircularProgressIndicator(color: Variables.textPrimary))
- : (_stylesheetMap == null && _rawJsonString == null)
+ : (_stylesheetMap == null && _rawJsonString == null && _projectAssets.isEmpty)
? _buildEmptyState()
: _buildContent(),
bottomNavigationBar: BottomBar(
@@ -228,8 +264,11 @@ class _StylesheetPageState extends State<StylesheetPage> {
final emotions = _getData(['Emotions', 'emotions']);
final era = _getData(['Era/Cultural Reference', 'era']);
final typography = _getData(['Typography', 'fonts']);
-
- final foundAny = (style != null || lighting != null || colors != null || emotions != null || era != null || typography != null);
+
+ // We check _projectAssets.isNotEmpty to determine if we show the section
+ final hasAssets = _projectAssets.isNotEmpty;
+
+ final foundAny = (style != null || lighting != null || colors != null || emotions != null || era != null || typography != null || hasAssets);
return RefreshIndicator(
onRefresh: _generateStylesheet,
@@ -257,11 +296,15 @@ class _StylesheetPageState extends State<StylesheetPage> {
const SizedBox(height: 32),
if (foundAny) ...[
- // 1. Typography (Now a Slider)
+ // 1. Assets / Subjects Section (From Project Repo)
+ if (hasAssets)
+ _buildAssetsSection(_projectAssets),
+
+ // 2. Typography (Now a Slider)
if (typography != null)
_buildTypographySection(typography),
- // 2. Color Palette
+ // 3. Color Palette
if (colors != null) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
@@ -276,7 +319,7 @@ class _StylesheetPageState extends State<StylesheetPage> {
const SizedBox(height: 32),
],
- // 3. Slider Sections
+ // 4. Slider Sections
if (style != null) _buildSliderSection("Style & Aesthetic", style),
if (emotions != null) _buildSliderSection("Mood & Emotions", emotions),
if (lighting != null) _buildSliderSection("Lighting", lighting),
@@ -333,6 +376,92 @@ class _StylesheetPageState extends State<StylesheetPage> {
);
}
+ // --- ASSETS SECTION ---
+ Widget _buildAssetsSection(List<String> imagePaths) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: _buildSectionHeader("Subjects & Assets"),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: GridView.builder(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 3,
+ crossAxisSpacing: 12,
+ mainAxisSpacing: 12,
+ childAspectRatio: 0.8,
+ ),
+ itemCount: imagePaths.length,
+ itemBuilder: (context, index) => _buildAssetCard(imagePaths[index]),
+ ),
+ ),
+ const SizedBox(height: 32),
+ ],
+ );
+ }
+
+ Widget _buildAssetCard(String savedPath) {
+ return FutureBuilder<File?>(
+ future: _resolveFile(savedPath),
+ builder: (context, snapshot) {
+ final File? file = snapshot.data;
+ final bool exists = file != null;
+
+ return Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12), // Matching color card radius
+ border: Border.all(color: Variables.borderSubtle),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.03),
+ blurRadius: 8,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ clipBehavior: Clip.antiAlias,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Expanded(
+ child: exists
+ ? Image.file(file, fit: BoxFit.cover)
+ : Container(
+ color: Colors.grey.shade100,
+ child: const Center(
+ child: Icon(Icons.broken_image, color: Colors.grey, size: 20),
+ ),
+ ),
+ ),
+ // Container(
+ // padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
+ // color: Colors.white,
+ // child: const Text(
+ // "Asset",
+ // textAlign: TextAlign.center,
+ // style: TextStyle(
+ // fontFamily: 'GeneralSans',
+ // fontSize: 10,
+ // fontWeight: FontWeight.w600,
+ // color: Variables.textPrimary,
+ // ),
+ // maxLines: 1,
+ // overflow: TextOverflow.ellipsis,
+ // ),
+ // )
+ ],
+ ),
+ );
+ },
+ );
+ }
+
// --- TYPOGRAPHY SECTION (UPDATED TO SLIDER) ---
Widget _buildTypographySection(dynamic data) {
List<String> fontNames = [];