commit a77485574f710776324dee39cfa23fb5a087095e
parent 113bf2aacbb84de7e90a89ef44090d27534a161d
Author: Sanjeebani Parida <83271316+sanjeebani14@users.noreply.github.com>
Date: Tue, 2 Dec 2025 01:22:33 +0530
Merge pull request #29 from nilotpal-n7/sanjeebani
Sanjeebani
Diffstat:
6 files changed, 1197 insertions(+), 940 deletions(-)
diff --git a/lib/data/repos/file_repo.dart b/lib/data/repos/file_repo.dart
@@ -8,6 +8,25 @@ class FileRepo {
await db.insert('files', file.toMap());
}
+ // New method: Gets all files, regardless of project ID, ordered by last_updated
+ Future<List<FileModel>> getAllFiles() async {
+ final db = await AppDatabase.db;
+ final res = await db.query('files', orderBy: 'last_updated DESC');
+ return res.map((e) => FileModel.fromMap(e)).toList();
+ }
+
+ // New method: Gets the most recent files across all projects, capped by limit
+ Future<List<FileModel>> getRecentFiles({int limit = 10}) async {
+ final db = await AppDatabase.db;
+ final res = await db.query(
+ 'files',
+ orderBy: 'last_updated DESC',
+ limit: limit,
+ );
+ return res.map((e) => FileModel.fromMap(e)).toList();
+ }
+
+ // Retained original function for fetching files by a specific project ID
Future<List<FileModel>> getFiles(int projectId) async {
final db = await AppDatabase.db;
final res = await db.query(
@@ -70,25 +89,38 @@ class FileRepo {
return res.map((e) => e['file_path'] as String).toList();
}
- Future<FileModel?> getByFilePath(String path) async {
+ Future<List<FileModel>> getFilesForProjectAndEvents(int projectId) async {
final db = await AppDatabase.db;
+
+ // Fetch events of this project
+ final eventRows = await db.query(
+ 'projects',
+ where: 'parent_id = ?',
+ whereArgs: [projectId],
+ );
+
+ final eventIds = eventRows.map((e) => e['id'] as int).toList();
+ final allIds = [projectId, ...eventIds];
+
final res = await db.query(
'files',
- where: 'file_path = ?',
- whereArgs: [path],
- limit: 1,
+ where: 'project_id IN (${List.filled(allIds.length, '?').join(',')})',
+ whereArgs: allIds,
+ orderBy: 'last_updated DESC',
);
- if (res.isNotEmpty) return FileModel.fromMap(res.first);
- return null;
+
+ return res.map((e) => FileModel.fromMap(e)).toList();
}
- Future<List<FileModel>> getRecentFiles({int limit = 10}) async {
+ Future<FileModel?> getByFilePath(String path) async {
final db = await AppDatabase.db;
final res = await db.query(
'files',
- orderBy: 'last_updated DESC',
- limit: limit,
+ where: 'file_path = ?',
+ whereArgs: [path],
+ limit: 1,
);
- return res.map((e) => FileModel.fromMap(e)).toList();
+ if (res.isNotEmpty) return FileModel.fromMap(res.first);
+ return null;
}
}
diff --git a/lib/services/file_service.dart b/lib/services/file_service.dart
@@ -37,10 +37,26 @@ class FileService {
return id;
}
+ // New method to get ALL files
+ Future<List<FileModel>> getAllFiles() async {
+ return await _repo.getAllFiles();
+ }
+
+ // New method to get recent files globally (limit 3 for the UI)
+ Future<List<FileModel>> getRecentFiles({int limit = 10}) async {
+ return await _repo.getRecentFiles(limit: limit);
+ }
+
+ // Retained for backwards compatibility, though generally deprecated for a global file list view.
+ @Deprecated('Use getAllFiles() or getRecentFiles() for the file list page.')
Future<List<FileModel>> getFiles(int projectId) async {
return await _repo.getFiles(projectId);
}
+ Future<List<FileModel>> getFilesForProjectAndEvents(int projectId) {
+ return _repo.getFilesForProjectAndEvents(projectId);
+ }
+
Future<void> openFile(String id) async {
await _repo.touchFile(id);
}
@@ -67,4 +83,10 @@ class FileService {
await _repo.deleteFile(id);
}
}
+
+ Future<void> renameFile(String id, String newName) async {
+ await _repo.updateDetails(id, name: newName);
+ }
+
+
}
diff --git a/lib/ui/pages/canvas_board_page.dart b/lib/ui/pages/canvas_board_page.dart
@@ -87,6 +87,7 @@ class CanvasBoardPage extends StatefulWidget {
final double height;
final File? initialImage;
final FileModel? existingFile;
+ final File? injectedMedia;
const CanvasBoardPage({
super.key,
@@ -95,6 +96,7 @@ class CanvasBoardPage extends StatefulWidget {
required this.height,
this.initialImage,
this.existingFile,
+ this.injectedMedia,
});
@override
@@ -172,6 +174,26 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
});
_hasUnsavedChanges = true;
}
+
+ // Inject shared image EXACTLY like gallery images
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (widget.injectedMedia != null && widget.existingFile != null) {
+ final oldState = _getCurrentState();
+ setState(() {
+ elements.add({
+ 'id': 'shared_${DateTime.now().millisecondsSinceEpoch}',
+ 'type': 'file_image',
+ 'content': widget.injectedMedia!.path,
+ 'position': const Offset(50, 50),
+ 'size': const Size(150, 150), // EXACT match
+ 'rotation': 0.0,
+ });
+ _hasUnsavedChanges = true;
+ });
+ _recordChange(oldState);
+ }
+ });
+
}
@override
@@ -1469,6 +1491,20 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
}
_hasUnsavedChanges = false;
});
+ if (widget.injectedMedia != null) {
+ final oldState = _getCurrentState();
+ setState(() {
+ elements.add({
+ 'id': 'shared_${DateTime.now().millisecondsSinceEpoch}',
+ 'type': 'file_image',
+ 'content': widget.injectedMedia!.path,
+ 'position': const Offset(50, 50),
+ 'size': const Size(150, 150),
+ 'rotation': 0.0,
+ });
+ });
+ _recordChange(oldState);
+ }
}
} catch (e) {
debugPrint("Error loading canvas: $e");
diff --git a/lib/ui/pages/create_file_page.dart b/lib/ui/pages/create_file_page.dart
@@ -12,9 +12,10 @@ import '../../services/image_service.dart';
import '../../data/models/project_model.dart';
class CreateFilePage extends StatefulWidget {
- final File? file; // Made optional for blank canvas creation
- final int projectId;
- const CreateFilePage({super.key, this.file, this.projectId = 0});
+ final File? file;
+ final int? projectId; // ⭐ MAKE NULLABLE
+
+ const CreateFilePage({super.key, this.file, this.projectId});
@override
State<CreateFilePage> createState() => _CreateFilePageState();
@@ -23,11 +24,11 @@ class CreateFilePage extends StatefulWidget {
class _CreateFilePageState extends State<CreateFilePage> {
final TextEditingController _searchController = TextEditingController();
- // Project Selection State
+ // Project Selection
int? _selectedProjectId;
String _selectedProjectTitle = "Select Project";
- // Master list of presets
+ // Presets
final List<CanvasPreset> _allPresets = [
CanvasPreset(
name: 'Custom',
@@ -95,7 +96,7 @@ class _CreateFilePageState extends State<CreateFilePage> {
super.initState();
_filteredPresets = _allPresets;
- // Initialize from passed project ID
+ // ⭐ If an existing projectId was passed, lock to that project
_selectedProjectId = widget.projectId;
if (_selectedProjectId != null) {
_selectedProjectTitle = "Current Project";
@@ -108,55 +109,76 @@ class _CreateFilePageState extends State<CreateFilePage> {
super.dispose();
}
- void _runFilter(String enteredKeyword) {
- List<CanvasPreset> results = [];
- if (enteredKeyword.isEmpty) {
- results = _allPresets;
- } else {
- results =
+ void _runFilter(String keyword) {
+ if (keyword.isEmpty) {
+ setState(() => _filteredPresets = _allPresets);
+ return;
+ }
+
+ setState(() {
+ _filteredPresets =
_allPresets
.where(
- (preset) => preset.name.toLowerCase().contains(
- enteredKeyword.toLowerCase(),
- ),
+ (p) => p.name.toLowerCase().contains(keyword.toLowerCase()),
)
.toList();
- }
- setState(() {
- _filteredPresets = results;
});
}
+ // ⭐ Select project (only for ShareToFilePage flow)
void _openProjectSelection() {
showModalBottomSheet(
context: context,
- isScrollControlled: true,
backgroundColor: Colors.transparent,
- builder:
- (context) => DraggableScrollableSheet(
- initialChildSize: 0.85,
- minChildSize: 0.5,
- maxChildSize: 0.95,
- builder:
- (_, controller) => Container(
- decoration: const BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.vertical(
- top: Radius.circular(20),
- ),
- ),
- child: ProjectSelectionModal(
- scrollController: controller,
- onProjectSelected: (id, title) {
- setState(() {
- _selectedProjectId = id;
- _selectedProjectTitle = title;
- });
- Navigator.pop(context);
- },
- ),
- ),
- ),
+ isScrollControlled: true,
+ builder: (_) {
+ return DraggableScrollableSheet(
+ initialChildSize: 0.85,
+ maxChildSize: 0.95,
+ minChildSize: 0.5,
+ builder: (_, controller) {
+ return Container(
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ child: ProjectSelectionModal(
+ scrollController: controller,
+ onProjectSelected: (id, title) {
+ setState(() {
+ _selectedProjectId = id;
+ _selectedProjectTitle = title;
+ });
+ Navigator.pop(context);
+ },
+ ),
+ );
+ },
+ );
+ },
+ );
+ }
+
+ // ⭐ Go to canvas with selected project
+ void _navigateToEditor(int width, int height) {
+ if (_selectedProjectId == null) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text("Please select a destination project")),
+ );
+ return;
+ }
+
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder:
+ (_) => CanvasBoardPage(
+ projectId: _selectedProjectId!, // ⭐ use selected project
+ width: width.toDouble(),
+ height: height.toDouble(),
+ initialImage: widget.file,
+ ),
+ ),
);
}
@@ -174,27 +196,28 @@ class _CreateFilePageState extends State<CreateFilePage> {
title: const Text(
'Create Files',
style: TextStyle(
- color: Colors.black,
- fontWeight: FontWeight.w600,
fontSize: 16,
+ fontWeight: FontWeight.w600,
+ color: Colors.black,
),
),
+
+ // ⭐ Show project chooser ONLY when projectId was NOT passed
actions: [
- // ONLY show selection button if projectId was NOT passed in
if (widget.projectId == null)
Padding(
- padding: const EdgeInsets.only(right: 16.0),
+ padding: const EdgeInsets.only(right: 16),
child: TextButton.icon(
onPressed: _openProjectSelection,
style: TextButton.styleFrom(
backgroundColor: Colors.white,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(20),
- ),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(20),
+ ),
),
icon: Icon(
_selectedProjectId == null
@@ -206,21 +229,20 @@ class _CreateFilePageState extends State<CreateFilePage> {
label: Text(
_selectedProjectTitle,
style: const TextStyle(
- color: Colors.black,
fontSize: 12,
fontWeight: FontWeight.w500,
+ color: Colors.black,
),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
),
),
),
],
),
+
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- // Search Bar
+ // Search
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Container(
@@ -233,7 +255,6 @@ class _CreateFilePageState extends State<CreateFilePage> {
onChanged: _runFilter,
decoration: InputDecoration(
hintText: 'Search sizes',
- hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
prefixIcon: Icon(
Icons.search,
color: Colors.grey[400],
@@ -263,7 +284,6 @@ class _CreateFilePageState extends State<CreateFilePage> {
),
),
- // Label
const Padding(
padding: EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Text(
@@ -276,27 +296,6 @@ class _CreateFilePageState extends State<CreateFilePage> {
),
),
- // Tabs
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- child: Row(
- children: [
- _buildTab('All', isSelected: true),
- const SizedBox(width: 12),
- _buildTab('Saved', isSelected: false),
- const SizedBox(width: 12),
- _buildTab('Photo', isSelected: false),
- const SizedBox(width: 12),
- _buildTab('Print', isSelected: false),
- ],
- ),
- ),
- ),
-
- const SizedBox(height: 16),
-
// Grid
Expanded(
child: GridView.builder(
@@ -308,9 +307,7 @@ class _CreateFilePageState extends State<CreateFilePage> {
childAspectRatio: 0.75,
),
itemCount: _filteredPresets.length,
- itemBuilder: (context, index) {
- return _buildPresetCard(_filteredPresets[index]);
- },
+ itemBuilder: (_, i) => _buildPresetCard(_filteredPresets[i]),
),
),
],
@@ -318,34 +315,16 @@ class _CreateFilePageState extends State<CreateFilePage> {
);
}
- Widget _buildTab(String label, {required bool isSelected}) {
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
- decoration: BoxDecoration(
- color: isSelected ? Colors.white : Colors.transparent,
- borderRadius: BorderRadius.circular(20),
- ),
- child: Text(
- label,
- style: TextStyle(
- color: isSelected ? Colors.black : Colors.grey,
- fontSize: 13,
- fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal,
- ),
- ),
- );
- }
-
+ // Card widget
Widget _buildPresetCard(CanvasPreset preset) {
final bool isCustom = preset.name == 'Custom';
return InkWell(
onTap: () {
- if (isCustom) {
- _navigateToEditor(1000, 1000);
- } else {
- _navigateToEditor(preset.width, preset.height);
- }
+ _navigateToEditor(
+ isCustom ? 1000 : preset.width,
+ isCustom ? 1000 : preset.height,
+ );
},
borderRadius: BorderRadius.circular(12),
child: Container(
@@ -355,12 +334,10 @@ class _CreateFilePageState extends State<CreateFilePage> {
),
padding: const EdgeInsets.all(8),
child: Column(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
- width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFFE0E7FF),
borderRadius: BorderRadius.circular(8),
@@ -368,40 +345,30 @@ class _CreateFilePageState extends State<CreateFilePage> {
child: Center(
child:
isCustom
- ? const Icon(Icons.add, size: 30, color: Colors.blue)
+ ? const Icon(Icons.add, color: Colors.blue, size: 30)
: Padding(
- padding: const EdgeInsets.all(12.0),
+ padding: const EdgeInsets.all(12),
child: AspectRatio(
- aspectRatio:
- (preset.width > 0 && preset.height > 0)
- ? preset.width / preset.height
- : 1.0,
+ aspectRatio: preset.width / preset.height,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
- color: Colors.black.withValues(
- alpha: 0.05,
- ),
+ color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
- child: Center(
- child:
- preset.svgPath != null
- ? Padding(
- padding: const EdgeInsets.all(4.0),
- child: SvgPicture.asset(
- preset.svgPath!,
- fit: BoxFit.contain,
- ),
- )
- : null,
- ),
+ child:
+ preset.svgPath != null
+ ? SvgPicture.asset(
+ preset.svgPath!,
+ fit: BoxFit.contain,
+ )
+ : null,
),
),
),
@@ -409,56 +376,25 @@ class _CreateFilePageState extends State<CreateFilePage> {
),
),
const SizedBox(height: 8),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- preset.name,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(
- fontWeight: FontWeight.w600,
- fontSize: 11,
- color: Colors.black,
- ),
- ),
- const SizedBox(height: 2),
- Text(
- preset.displaySize,
- style: const TextStyle(color: Colors.grey, fontSize: 9),
- ),
- ],
+ Text(
+ preset.name,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: const TextStyle(
+ fontWeight: FontWeight.w600,
+ fontSize: 11,
+ color: Colors.black,
+ ),
+ ),
+ Text(
+ preset.displaySize,
+ style: const TextStyle(color: Colors.grey, fontSize: 9),
),
],
),
),
);
}
-
- void _navigateToEditor(int width, int height) {
- // FORCE Selection: If no project is selected, open the modal and return.
- if (_selectedProjectId == null) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text("Please select a destination project")),
- );
- _openProjectSelection();
- return;
- }
-
- Navigator.push(
- context,
- MaterialPageRoute(
- builder:
- (context) => CanvasBoardPage(
- projectId: widget.projectId,
- // Pass the dimensions from the preset
- width: width.toDouble(),
- height: height.toDouble(),
- initialImage: widget.file,
- ),
- ),
- );
- }
}
class CanvasPreset {
@@ -478,7 +414,7 @@ class CanvasPreset {
}
// --------------------------------------------------------------------------
-// --- PROJECT SELECTION MODAL ---
+// --- PROJECT SELECTION MODAL ----------------------------------------------
// --------------------------------------------------------------------------
class ProjectItemViewModel {
@@ -531,6 +467,7 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> {
bool _isLoading = true;
String _searchQuery = "";
+
final TextEditingController _searchController = TextEditingController();
@override
@@ -551,21 +488,19 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> {
if (images.isNotEmpty) {
return images.first.filePath;
}
- } catch (e) {
- debugPrint("Error fetching cover for project $projectId: $e");
- }
+ } catch (_) {}
return null;
}
Future<void> _loadData() async {
setState(() => _isLoading = true);
- final recentItems = await _projectService.getRecentProjectsAndEvents();
+ final recent = await _projectService.getRecentProjectsAndEvents();
final allProjects = await _projectService.getAllProjects();
- // Build Recent View Models
+ // Build Recent
final List<ProjectItemViewModel> recents = [];
- for (var item in recentItems.take(3)) {
+ for (var item in recent.take(3)) {
String? parentTitle;
if (item.parentId != null) {
final parent = await _projectService.getProjectById(item.parentId!);
@@ -582,17 +517,17 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> {
}
_recentViewModels = recents;
- // Build Grouped Projects
+ // Build groups
final List<ProjectGroup> groups = [];
for (final p in allProjects) {
- final rawEvents = await _projectService.getEvents(p.id!);
- final List<ProjectItemViewModel> eventVMs = [];
- for (final e in rawEvents) {
- final eCover = await _getProjectCover(e.id!);
- eventVMs.add(ProjectItemViewModel(item: e, coverPath: eCover));
+ final eventsRaw = await _projectService.getEvents(p.id!);
+ final List<ProjectItemViewModel> events = [];
+ for (final e in eventsRaw) {
+ final cover = await _getProjectCover(e.id!);
+ events.add(ProjectItemViewModel(item: e, coverPath: cover));
}
- final pCover = await _getProjectCover(p.id!);
- groups.add(ProjectGroup(project: p, events: eventVMs, coverPath: pCover));
+ final cover = await _getProjectCover(p.id!);
+ groups.add(ProjectGroup(project: p, events: events, coverPath: cover));
}
_groupedProjects = groups;
@@ -604,60 +539,53 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> {
void _filterProjects(String query) {
setState(() {
_searchQuery = query;
+
if (query.isEmpty) {
_filteredGroupedProjects = _groupedProjects;
- } else {
- final q = query.toLowerCase();
- final List<ProjectGroup> filtered = [];
- for (final g in _groupedProjects) {
- final projectMatch = g.project.title.toLowerCase().contains(q);
- final matchingEvents =
- g.events.where((e) => e.title.toLowerCase().contains(q)).toList();
-
- if (projectMatch) {
- filtered.add(
- ProjectGroup(
- project: g.project,
- events: g.events,
- isExpanded: true,
- coverPath: g.coverPath,
- ),
- );
- } else if (matchingEvents.isNotEmpty) {
- filtered.add(
- ProjectGroup(
- project: g.project,
- events: matchingEvents,
- isExpanded: true,
- coverPath: g.coverPath,
- ),
- );
- }
+ return;
+ }
+
+ final q = query.toLowerCase();
+ final List<ProjectGroup> filtered = [];
+
+ for (final g in _groupedProjects) {
+ final projectMatch = g.project.title.toLowerCase().contains(q);
+ final matchingEvents =
+ g.events.where((e) => e.title.toLowerCase().contains(q)).toList();
+
+ if (projectMatch) {
+ filtered.add(
+ ProjectGroup(
+ project: g.project,
+ events: g.events,
+ isExpanded: true,
+ coverPath: g.coverPath,
+ ),
+ );
+ } else if (matchingEvents.isNotEmpty) {
+ filtered.add(
+ ProjectGroup(
+ project: g.project,
+ events: matchingEvents,
+ isExpanded: true,
+ coverPath: g.coverPath,
+ ),
+ );
}
- _filteredGroupedProjects = filtered;
}
+
+ _filteredGroupedProjects = filtered;
});
}
Future<void> _createNewProject() async {
final result = await Navigator.push(
context,
- MaterialPageRoute(
- builder:
- (context) => const DefineBrandPage(
- projectName:
- "", // Can pass empty string if you want user to type it
- ),
- ),
+ MaterialPageRoute(builder: (_) => const DefineBrandPage(projectName: "")),
);
- // Check if a project was created and returned
if (result != null && result is Map) {
- final newId = result['id'];
- final title = result['title'];
- if (newId != null && title != null) {
- widget.onProjectSelected(newId, title);
- }
+ widget.onProjectSelected(result["id"], result["title"]);
}
}
@@ -665,12 +593,11 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> {
Widget build(BuildContext context) {
return Column(
children: [
- // Header handle
Center(
child: Container(
- margin: const EdgeInsets.only(top: 12, bottom: 8),
width: 40,
height: 4,
+ margin: const EdgeInsets.only(top: 12, bottom: 8),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
@@ -678,109 +605,72 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> {
),
),
- // Header Row
+ // Header row
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Select Destination",
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- fontFamily: 'GeneralSans',
- ),
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: _createNewProject,
- tooltip: "Create New Project",
),
],
),
),
- // Search Bar
+ // Search bar
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- child: SizedBox(
- height: 42,
- child: TextField(
- controller: _searchController,
- onChanged: _filterProjects,
- decoration: InputDecoration(
- hintText: "Search Projects",
- prefixIcon: const Icon(
- Icons.search,
- size: 20,
- color: Color(0xFF9F9FA9),
- ),
- filled: true,
- fillColor: const Color(0xFFE4E4E7),
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8),
- borderSide: BorderSide.none,
- ),
- contentPadding: const EdgeInsets.symmetric(
- vertical: 0,
- horizontal: 16,
- ),
+ child: TextField(
+ controller: _searchController,
+ onChanged: _filterProjects,
+ decoration: InputDecoration(
+ hintText: "Search Projects",
+ prefixIcon: const Icon(Icons.search, size: 20),
+ filled: true,
+ fillColor: const Color(0xFFE4E4E7),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: BorderSide.none,
),
),
),
),
- Divider(color: Colors.grey[200]),
+ Divider(color: Colors.grey[300]),
- // Content
Expanded(
child:
_isLoading
? const Center(child: CircularProgressIndicator())
: ListView(
controller: widget.scrollController,
- padding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 8,
- ),
children: [
- if (_searchQuery.isEmpty &&
- _recentViewModels.isNotEmpty) ...[
+ if (_searchQuery.isEmpty && _recentViewModels.isNotEmpty)
const Padding(
- padding: EdgeInsets.only(bottom: 8, top: 8),
+ padding: EdgeInsets.only(left: 16, bottom: 8),
child: Text(
"Recent Projects/Events",
- style: TextStyle(
- fontSize: 14,
- fontFamily: 'GeneralSans',
- color: Color(0xFF27272A),
- fontWeight: FontWeight.w400,
- ),
+ style: TextStyle(fontSize: 14),
),
),
- ..._recentViewModels.map((vm) => _buildRecentItem(vm)),
- const SizedBox(height: 16),
- ],
+ if (_searchQuery.isEmpty)
+ ..._recentViewModels.map(_buildRecentItem),
- Padding(
- padding: const EdgeInsets.only(bottom: 8),
+ const Padding(
+ padding: EdgeInsets.only(left: 16, top: 8, bottom: 8),
child: Text(
- _searchQuery.isEmpty
- ? "All Projects/Events"
- : "Search Results",
- style: const TextStyle(
- fontSize: 14,
- fontFamily: 'GeneralSans',
- color: Color(0xFF27272A),
- fontWeight: FontWeight.w400,
- ),
+ "All Projects/Events",
+ style: TextStyle(fontSize: 14),
),
),
- ..._filteredGroupedProjects.map(
- (g) => _buildProjectGroup(g),
- ),
- const SizedBox(height: 40),
+
+ ..._filteredGroupedProjects.map(_buildProjectGroup),
],
),
),
@@ -789,211 +679,79 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> {
}
Widget _buildRecentItem(ProjectItemViewModel vm) {
+ return InkWell(
+ onTap: () => widget.onProjectSelected(vm.id, vm.title),
+ child: ListTile(
+ leading: _thumbnail(vm.coverPath),
+ title: Text(vm.title),
+ subtitle: vm.parentTitle != null ? Text(vm.parentTitle!) : null,
+ ),
+ );
+ }
+
+ Widget _thumbnail(String? path) {
return Container(
- margin: const EdgeInsets.only(bottom: 8),
- child: InkWell(
- onTap: () => widget.onProjectSelected(vm.id, vm.title),
- borderRadius: BorderRadius.circular(16),
- child: Container(
- padding: const EdgeInsets.fromLTRB(4, 4, 0, 4),
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: const Color(0xFFE4E4E7), width: 1),
- ),
- child: Row(
- children: [
- Container(
- width: 56,
- height: 56,
- decoration: BoxDecoration(
- color: const Color(0xFFFAFAFA),
- borderRadius: BorderRadius.circular(8),
- image:
- vm.coverPath != null
- ? DecorationImage(
- image: FileImage(File(vm.coverPath!)),
- fit: BoxFit.cover,
- )
- : null,
- ),
- child:
- vm.coverPath == null
- ? Icon(Icons.image, color: Colors.grey[400], size: 28)
- : null,
- ),
- const SizedBox(width: 10),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- if (vm.isEvent && vm.parentTitle != null)
- Padding(
- padding: const EdgeInsets.only(bottom: 2),
- child: Text(
- vm.parentTitle!,
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 12,
- color: Color(0xFF27272A),
- fontWeight: FontWeight.w400,
- ),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- Text(
- vm.title,
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 16,
- fontWeight: FontWeight.w500,
- color: Color(0xFF27272A),
- ),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- ],
- ),
- ),
- ],
- ),
- ),
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: Colors.grey[200],
+ borderRadius: BorderRadius.circular(8),
+ image:
+ path != null
+ ? DecorationImage(
+ image: FileImage(File(path)),
+ fit: BoxFit.cover,
+ )
+ : null,
),
+ child: path == null ? Icon(Icons.image, color: Colors.grey[400]) : null,
);
}
Widget _buildProjectGroup(ProjectGroup g) {
final hasEvents = g.events.isNotEmpty;
+
return Container(
margin: const EdgeInsets.only(bottom: 8),
- clipBehavior: Clip.antiAlias,
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: const Color(0xFFE4E4E7)),
- ),
child: Column(
children: [
ListTile(
- onTap:
- () =>
- hasEvents
- ? setState(() => g.isExpanded = !g.isExpanded)
- : widget.onProjectSelected(
- g.project.id!,
- g.project.title,
- ),
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 4,
- ),
- visualDensity: VisualDensity.compact,
- leading: Container(
- width: 48,
- height: 48,
- decoration: BoxDecoration(
- color: Colors.grey[100],
- borderRadius: BorderRadius.circular(12),
- image:
- g.coverPath != null
- ? DecorationImage(
- image: FileImage(File(g.coverPath!)),
- fit: BoxFit.cover,
- )
- : null,
- ),
- child:
- g.coverPath == null
- ? Icon(Icons.folder, color: Colors.grey[500])
- : null,
- ),
- title: Text(
- g.project.title,
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 16,
- fontWeight: FontWeight.w500,
- color: Color(0xFF27272A),
- ),
- ),
+ onTap: () {
+ if (hasEvents) {
+ setState(() => g.isExpanded = !g.isExpanded);
+ } else {
+ widget.onProjectSelected(g.project.id!, g.project.title);
+ }
+ },
+ leading: _thumbnail(g.coverPath),
+ title: Text(g.project.title),
trailing:
hasEvents
- ? IconButton(
- icon: Icon(
- g.isExpanded
- ? Icons.keyboard_arrow_up
- : Icons.keyboard_arrow_down,
- color: Colors.grey[600],
- ),
- onPressed: () {
- setState(() => g.isExpanded = !g.isExpanded);
- },
+ ? Icon(
+ g.isExpanded
+ ? Icons.keyboard_arrow_up
+ : Icons.keyboard_arrow_down,
)
: null,
),
+
if (hasEvents && g.isExpanded)
- AnimatedCrossFade(
- firstChild: const SizedBox.shrink(),
- secondChild: Container(
- width: double.infinity,
- color: const Color(0xFFF9FAFB),
- child: Column(
- children:
- g.events.map((e) {
- return ListTile(
- onTap: () => widget.onProjectSelected(e.id, e.title),
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 24,
- vertical: 2,
- ),
- visualDensity: VisualDensity.compact,
- leading: Container(
- width: 40,
- height: 40,
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(10),
- border: Border.all(color: Colors.grey.shade200),
- image:
- e.coverPath != null
- ? DecorationImage(
- image: FileImage(File(e.coverPath!)),
- fit: BoxFit.cover,
- )
- : null,
- ),
- child:
- e.coverPath == null
- ? const Icon(
- Icons.event,
- size: 20,
- color: Colors.grey,
- )
- : null,
- ),
- title: Text(
- e.title,
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 15,
- fontWeight: FontWeight.w500,
- color: Color(0xFF27272A),
- ),
- ),
- );
- }).toList(),
- ),
+ Container(
+ color: Colors.grey[100],
+ child: Column(
+ children:
+ g.events.map((e) {
+ return ListTile(
+ onTap: () => widget.onProjectSelected(e.id, e.title),
+ leading: _thumbnail(e.coverPath),
+ title: Text(e.title),
+ );
+ }).toList(),
),
- crossFadeState:
- g.isExpanded
- ? CrossFadeState.showSecond
- : CrossFadeState.showFirst,
- duration: const Duration(milliseconds: 200),
),
],
),
);
}
}
+
diff --git a/lib/ui/pages/project_file_page.dart b/lib/ui/pages/project_file_page.dart
@@ -1,12 +1,17 @@
import 'dart:io';
+import 'dart:convert';
import 'package:flutter/material.dart';
-import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
+
import '../../data/models/file_model.dart';
+import '../../data/models/project_model.dart';
import '../../services/file_service.dart';
+import '../../data/repos/project_repo.dart';
import '../widgets/bottom_bar.dart';
import 'create_file_page.dart';
-import 'canvas_board_page.dart'; // [IMPORTANT] Import this to open the canvas
+import 'canvas_board_page.dart';
+
+import 'package:image/image.dart' as img;
class ProjectFilePage extends StatefulWidget {
final int projectId;
@@ -19,273 +24,529 @@ class ProjectFilePage extends StatefulWidget {
class _ProjectFilePageState extends State<ProjectFilePage> {
final _fileService = FileService();
- final _imagePicker = ImagePicker();
+ final _projectRepo = ProjectRepo();
+
+ List<FileModel> _allFiles = [];
+ List<ProjectModel> _events = [];
+ List<FileModel> _eventFiles = [];
+
+ Map<String, Map<String, String>> _fileMetadata = {};
- List<FileModel> _files = [];
+ ProjectModel? _selectedEvent;
bool _isLoading = true;
+ String _search = '';
@override
void initState() {
super.initState();
- _loadData();
+ _loadEverything();
}
- Future<void> _loadData() async {
+ // ---------------------------------------
+ // LOAD EVERYTHING
+ // ---------------------------------------
+ Future<void> _loadEverything() async {
setState(() => _isLoading = true);
try {
- final files = await _fileService.getFiles(widget.projectId);
- if (mounted) {
- setState(() {
- _files = files;
- _isLoading = false;
- });
+ _events = await _projectRepo.getEvents(widget.projectId);
+
+ _allFiles = await _fileService.getFilesForProjectAndEvents(
+ widget.projectId,
+ );
+
+ await _loadMetadata(_allFiles);
+
+ if (_events.isNotEmpty) {
+ _selectedEvent = _events.first;
+ _eventFiles = await _fileService.getFiles(_selectedEvent!.id!);
+ await _loadMetadata(_eventFiles);
}
} catch (e) {
- debugPrint('Error loading files: $e');
- if (mounted) setState(() => _isLoading = false);
+ debugPrint("Error loading project page: $e");
}
+
+ if (mounted) setState(() => _isLoading = false);
}
- // --- ACTIONS ---
+ // ---------------------------------------
+ // LOAD METADATA (Like HomePage)
+ // ---------------------------------------
+ Future<void> _loadMetadata(List<FileModel> list) async {
+ for (final file in list) {
+ try {
+ final f = File(file.filePath);
+ if (!await f.exists()) continue;
- void _navigateToCreateFile() {
+ if (file.filePath.toLowerCase().endsWith(".json")) {
+ final content = await f.readAsString();
+ final data = jsonDecode(content);
+
+ String dims = "Unknown";
+ String preview = "";
+
+ if (data is Map) {
+ if (data["width"] != null && data["height"] != null) {
+ dims = "${data["width"]} x ${data["height"]} px";
+ }
+ if (data["preview_path"] != null) {
+ preview = data["preview_path"];
+ }
+ }
+
+ // Fix relative preview path
+ if (preview.isNotEmpty && !File(preview).existsSync()) {
+ final base = f.parent.path;
+ final candidate = "$base/$preview";
+ if (File(candidate).existsSync()) preview = candidate;
+ }
+
+ _fileMetadata[file.id] = {"preview": preview, "dimensions": dims};
+ } else {
+ final bytes = await f.readAsBytes();
+ final decoded = img.decodeImage(bytes);
+
+ String dims = "Unknown";
+ if (decoded != null) {
+ dims = "${decoded.width} x ${decoded.height} px";
+ }
+
+ _fileMetadata[file.id] = {
+ "preview": file.filePath,
+ "dimensions": dims,
+ };
+ }
+ } catch (_) {}
+ }
+ }
+
+ // ---------------------------------------
+ // SELECT EVENT
+ // ---------------------------------------
+ Future<void> _onSelectEvent(ProjectModel event) async {
+ setState(() => _selectedEvent = event);
+ _eventFiles = await _fileService.getFiles(event.id!);
+ await _loadMetadata(_eventFiles);
+ setState(() {});
+ }
+
+ // ---------------------------------------
+ // OPEN FILE
+ // ---------------------------------------
+ void _openFile(FileModel file) {
Navigator.push(
context,
MaterialPageRoute(
builder:
- (_) => CreateFilePage(
- // Pass the current projectId so the file is saved to THIS project
- projectId: widget.projectId,
+ (_) => CanvasBoardPage(
+ projectId: file.projectId,
+ width: 1080,
+ height: 1920,
+ existingFile: file,
),
),
- ).then((_) => _loadData());
- }
-
- void _openFile(FileModel file) {
- // Check if it's a Canvas file (JSON)
- if (file.filePath.toLowerCase().endsWith('.json')) {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder:
- (context) => CanvasBoardPage(
- projectId: widget.projectId,
- // Use default dimensions for viewing; the canvas content defines the actual size
- width: 1080,
- height: 1920,
- existingFile: file, // [KEY FIX] Loads the saved canvas
- ),
- ),
- ).then((_) => _loadData());
- } else {
- // It's a regular image
- // You can add navigation to an Image Viewer page here if you have one
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text("Image preview coming soon")),
- );
- }
+ );
}
- Future<void> _pickAndAddFile() async {
- try {
- final XFile? pickedFile = await _imagePicker.pickImage(
- source: ImageSource.gallery,
- );
+ void _handleFileMenuAction(FileModel file, String action) {
+ switch (action) {
+ case "open":
+ _openFile(file);
+ break;
- if (pickedFile != null && mounted) {
- // Navigate to CreateFilePage with the selected image
- await Navigator.push(
- context,
- MaterialPageRoute(
- builder:
- (_) => CreateFilePage(
- file: File(pickedFile.path),
- projectId: widget.projectId, // PASS PROJECT ID
- ),
- ),
- );
+ case "rename":
+ _renameFile(file);
+ break;
- // Refresh the list after returning from the creation flow
- if (mounted) {
- _loadData();
- }
- }
- } catch (e) {
- debugPrint("Error picking file: $e");
+ case "delete":
+ _deleteFile(file);
+ break;
}
}
- Future<void> _showAddFileDialog(File file) async {
- final nameController = TextEditingController(
- text: file.path.split('/').last,
- );
- final descriptionController = TextEditingController();
-
- if (!mounted) return;
+ Future<void> _renameFile(FileModel file) async {
+ final controller = TextEditingController(text: file.name);
- await showDialog(
+ final newName = await showDialog<String>(
context: context,
- builder:
- (context) => AlertDialog(
- title: const Text(
- "Save File",
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontWeight: FontWeight.w600,
- ),
+ builder: (context) {
+ return AlertDialog(
+ title: const Text("Rename File"),
+ content: TextField(
+ controller: controller,
+ autofocus: true,
+ decoration: const InputDecoration(
+ labelText: "New file name",
+ border: OutlineInputBorder(),
),
- content: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- TextField(
- controller: nameController,
- decoration: const InputDecoration(
- labelText: "File Name",
- border: OutlineInputBorder(),
- ),
- style: const TextStyle(fontFamily: 'GeneralSans'),
- ),
- const SizedBox(height: 16),
- TextField(
- controller: descriptionController,
- decoration: const InputDecoration(
- labelText: "Description (optional)",
- border: OutlineInputBorder(),
- ),
- maxLines: 2,
- style: const TextStyle(fontFamily: 'GeneralSans'),
- ),
- ],
- ),
- actions: [
- TextButton(
- onPressed: () => Navigator.pop(context),
- child: const Text("Cancel"),
- ),
- ElevatedButton(
- onPressed: () async {
- if (nameController.text.trim().isNotEmpty) {
- try {
- await _fileService.saveFile(
- file,
- widget.projectId,
- name: nameController.text.trim(),
- description:
- descriptionController.text.trim().isEmpty
- ? null
- : descriptionController.text.trim(),
- );
- if (context.mounted) {
- Navigator.pop(context);
- _loadData();
- }
- } catch (e) {
- debugPrint("Error saving file: $e");
- }
- }
- },
- child: const Text("Save"),
- ),
- ],
),
+ actions: [
+ TextButton(
+ child: const Text("Cancel"),
+ onPressed: () => Navigator.pop(context),
+ ),
+ FilledButton(
+ child: const Text("Save"),
+ onPressed: () => Navigator.pop(context, controller.text.trim()),
+ ),
+ ],
+ );
+ },
);
+
+ if (newName == null || newName.isEmpty) return;
+
+ // ✔ Update DB
+ await _fileService.renameFile(file.id, newName);
+
+ // ✔ Reload UI
+ await _loadEverything();
}
- Future<void> _deleteFile(String fileId) async {
+
+ Future<void> _deleteFile(FileModel file) async {
final confirm = await showDialog<bool>(
context: context,
builder:
- (context) => AlertDialog(
+ (_) => AlertDialog(
title: const Text("Delete File?"),
- content: const Text("This action cannot be undone."),
+ content: Text("This will permanently remove ${file.name}."),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text("Cancel"),
),
- TextButton(
+ FilledButton(
onPressed: () => Navigator.pop(context, true),
- style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text("Delete"),
),
],
),
);
- if (confirm == true) {
- await _fileService.deleteFile(fileId);
- _loadData();
+ if (confirm != true) return;
+
+ try {
+ await _fileService.deleteFile(file.id!);
+
+ final disk = File(file.filePath);
+ if (await disk.exists()) await disk.delete();
+
+ setState(() {
+ _allFiles.removeWhere((f) => f.id == file.id);
+ _eventFiles.removeWhere((f) => f.id == file.id);
+ });
+
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text("File deleted")));
+ } catch (e) {
+ debugPrint("Delete error: $e");
}
}
+
+ // ---------------------------------------
+ // CREATE FILE
+ // ---------------------------------------
+ void _navigateToCreateFile() {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => CreateFilePage(projectId: widget.projectId),
+ ),
+ ).then((_) => _loadEverything());
+ }
+
+ // ---------------------------------------
+ // FILE CARD (Same UI but thumbnail updated)
+ // ---------------------------------------
+ Widget _fileCard(FileModel file) {
+ final date = DateFormat.yMMMd().format(file.lastUpdated);
+ final meta = _fileMetadata[file.id] ?? {};
+ final preview = meta["preview"] ?? "";
+ final realPreview =
+ preview.isNotEmpty && File(preview).existsSync()
+ ? preview
+ : file.filePath;
+
+ return GestureDetector(
+ onTap: () => _openFile(file),
+ child: Container(
+ margin: const EdgeInsets.only(bottom: 12),
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: const Color(0xFFE4E4E7)),
+ ),
+ child: Row(
+ children: [
+ // ---------------------------------------
+ // ✔ NEW THUMBNAIL SIZE (HomePage style)
+ // ---------------------------------------
+ Container(
+ width: 80,
+ height: 80,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(8),
+ color: Colors.grey[300],
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: Image.file(
+ File(realPreview),
+ fit: BoxFit.cover,
+ errorBuilder: (_, __, ___) => const Icon(Icons.image),
+ ),
+ ),
+ ),
+
+ const SizedBox(width: 12),
+
+ // ---------------------------------------
+ // TEXT INFO (unchanged UI)
+ // ---------------------------------------
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ _breadcrumbFor(file),
+ style: TextStyle(
+ fontSize: 11,
+ color: Colors.grey[600],
+ fontFamily: 'GeneralSans',
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+
+ const SizedBox(height: 2),
+
+ Text(
+ file.name,
+ style: const TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'GeneralSans',
+ ),
+ ),
+
+ const SizedBox(height: 2),
+
+ Text(
+ "Edited $date",
+ style: TextStyle(
+ fontSize: 12,
+ color: Colors.grey[600],
+ fontFamily: 'GeneralSans',
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(width: 8),
+ PopupMenuButton<String>(
+ onSelected: (value) => _handleFileMenuAction(file, value),
+ itemBuilder:
+ (context) => [
+ const PopupMenuItem(value: "open", child: Text("Open")),
+ const PopupMenuItem(value: "rename", child: Text("Rename")),
+ const PopupMenuItem(value: "delete", child: Text("Delete")),
+ ],
+ icon: const Icon(Icons.more_vert, size: 20),
+ ),
+
+ ],
+ ),
+ ),
+ );
+ }
+
+ // ---------------------------------------
+ // BREADCRUMB
+ // ---------------------------------------
+ String _breadcrumbFor(FileModel file) {
+ final event = _events.firstWhere(
+ (e) => e.id == file.projectId,
+ orElse:
+ () => ProjectModel(
+ id: widget.projectId,
+ title: "",
+ lastAccessedAt: DateTime.now(),
+ createdAt: DateTime.now(),
+ ),
+ );
+
+ if (event.parentId == null) return "";
+
+ final parent = _events.firstWhere(
+ (e) => e.id == event.parentId,
+ orElse:
+ () => ProjectModel(
+ id: widget.projectId,
+ title: "",
+ lastAccessedAt: DateTime.now(),
+ createdAt: DateTime.now(),
+ ),
+ );
+
+ return "${parent.title} / ${event.title}";
+ }
+
+ // ---------------------------------------
+ // UI
+ // ---------------------------------------
@override
Widget build(BuildContext context) {
- final theme = Theme.of(context);
- final isDark = theme.brightness == Brightness.dark;
-
return Scaffold(
- backgroundColor: theme.scaffoldBackgroundColor,
+ backgroundColor: const Color(0xFFF7F7F8),
appBar: AppBar(
- title: const Text("Project Files"),
- backgroundColor: theme.appBarTheme.backgroundColor,
- actions: [
- IconButton(
- icon: const Icon(Icons.add),
- tooltip: "Create New File",
- onPressed: _navigateToCreateFile,
+ backgroundColor: const Color(0xFFF7F7F8),
+ elevation: 0,
+ title: const Text(
+ "Project Files",
+ style: TextStyle(
+ fontFamily: 'GeneralSans',
+ fontWeight: FontWeight.w500,
+ color: Color(0xFF27272A),
),
- ],
+ ),
),
+
bottomNavigationBar: BottomBar(
currentTab: BottomBarItem.files,
projectId: widget.projectId,
),
- floatingActionButton: FloatingActionButton(
- onPressed: _pickAndAddFile,
- child: const Icon(Icons.upload_file),
+
+ floatingActionButton: GestureDetector(
+ onTap: _navigateToCreateFile,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
+ decoration: BoxDecoration(
+ color: const Color(0xFF27272A),
+ borderRadius: BorderRadius.circular(50),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: const [
+ Text(
+ "Create File",
+ style: TextStyle(color: Colors.white, fontSize: 14),
+ ),
+ SizedBox(width: 8),
+ Icon(Icons.add, color: Colors.white),
+ ],
+ ),
+ ),
),
+
body:
_isLoading
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
- onRefresh: _loadData,
+ onRefresh: _loadEverything,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- _buildSectionHeader("Files", theme),
- Text(
- "${_files.length} items",
- style: TextStyle(
- color: theme.colorScheme.onSurface.withOpacity(
- 0.6,
- ),
- fontFamily: 'GeneralSans',
+ const SizedBox(height: 12),
+
+ _buildSearchBar(),
+
+ const SizedBox(height: 24),
+
+ // -------------------------
+ // ALL FILES
+ // -------------------------
+ const Text(
+ "All Files",
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'GeneralSans',
+ color: Color(0xFF27272A),
+ ),
+ ),
+
+ const SizedBox(height: 12),
+
+ if (_filteredAllFiles().isEmpty)
+ Center(
+ child: Padding(
+ padding: const EdgeInsets.all(32),
+ child: Text(
+ "No files found",
+ style: TextStyle(color: Colors.grey[500]),
),
),
- ],
+ )
+ else
+ Column(
+ children:
+ _filteredAllFiles()
+ .map((f) => _fileCard(f))
+ .toList(),
+ ),
+
+ const SizedBox(height: 32),
+
+ // -------------------------
+ // FILES FOR EVENTS
+ // -------------------------
+ const Text(
+ "Files for Events",
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'GeneralSans',
+ color: Color(0xFF27272A),
+ ),
),
- const SizedBox(height: 16),
- if (_files.isEmpty)
- _buildEmptyState(isDark)
+
+ const SizedBox(height: 12),
+
+ if (_events.isEmpty)
+ Container(
+ padding: const EdgeInsets.all(32),
+ decoration: BoxDecoration(
+ color: Colors.grey[100],
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ "No events yet",
+ style: TextStyle(color: Colors.grey[500]),
+ ),
+ )
else
- ListView.separated(
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- itemCount: _files.length,
- separatorBuilder:
- (_, __) => const SizedBox(height: 12),
- itemBuilder: (context, index) {
- final file = _files[index];
- return _buildFileCard(file, theme, isDark);
- },
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // -----------------------
+ // DROPDOWN ONLY
+ // -----------------------
+ _buildEventDropdown(),
+
+ const SizedBox(height: 16),
+
+ _eventFiles.isEmpty
+ ? Text(
+ "No files in this event yet",
+ style: TextStyle(color: Colors.grey[500]),
+ )
+ : Column(
+ children:
+ _eventFiles
+ .map((f) => _fileCard(f))
+ .toList(),
+ ),
+ ],
),
- const SizedBox(height: 40),
+
+ const SizedBox(height: 100),
],
),
),
@@ -293,168 +554,72 @@ class _ProjectFilePageState extends State<ProjectFilePage> {
);
}
- Widget _buildEmptyState(bool isDark) {
- return Container(
- width: double.infinity,
- padding: const EdgeInsets.all(32),
- decoration: BoxDecoration(
- color: isDark ? Colors.grey[850] : Colors.grey[100],
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: isDark ? Colors.grey[800]! : Colors.grey[300]!,
+ // ---------------------------------------
+ // SEARCH
+ // ---------------------------------------
+ Widget _buildSearchBar() {
+ return TextField(
+ onChanged: (v) => setState(() => _search = v.trim()),
+ decoration: InputDecoration(
+ filled: true,
+ fillColor: Colors.grey[200],
+ hintText: "Search your files",
+ prefixIcon: Icon(Icons.search, color: Colors.grey[600]),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: BorderSide.none,
),
),
- child: Column(
- children: [
- Icon(Icons.folder_open, size: 48, color: Colors.grey[400]),
- const SizedBox(height: 8),
- Text("No files added yet", style: TextStyle(color: Colors.grey[500])),
- ],
- ),
);
}
- Widget _buildSectionHeader(String title, ThemeData theme) {
- return Text(
- title,
- style: TextStyle(
- fontSize: 20,
- fontWeight: FontWeight.bold,
- fontFamily: 'GeneralSans',
- color: theme.colorScheme.onSurface,
+ // ---------------------------------------
+ // DROPDOWN (ONLY — add event removed)
+ // ---------------------------------------
+ Widget _buildEventDropdown() {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12),
+ decoration: BoxDecoration(
+ color: Colors.grey[200],
+ borderRadius: BorderRadius.circular(6),
),
- );
- }
-
- Widget _buildFileCard(FileModel file, ThemeData theme, bool isDark) {
- final dateStr = DateFormat.yMMMd().format(file.lastUpdated);
- final isCanvas = file.filePath.toLowerCase().endsWith('.json');
-
- return InkWell(
- onTap: () => _openFile(file), // [KEY FIX] Make the card clickable
- borderRadius: BorderRadius.circular(16),
- child: Container(
- decoration: BoxDecoration(
- color: isDark ? Colors.grey[850] : Colors.white,
- borderRadius: BorderRadius.circular(16),
- border: Border.all(
- color: isDark ? Colors.grey[700]! : Colors.grey[300]!,
- ),
- boxShadow: [
- BoxShadow(
- color: Colors.black.withOpacity(0.03),
- blurRadius: 8,
- offset: const Offset(0, 2),
- ),
- ],
- ),
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // Icon based on type
- Container(
- width: 48,
- height: 48,
- decoration: BoxDecoration(
- color: (isCanvas
- ? Colors.orange
- : theme.colorScheme.primary)
- .withOpacity(0.1),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Icon(
- isCanvas ? Icons.brush : Icons.insert_drive_file,
- color: isCanvas ? Colors.orange : theme.colorScheme.primary,
- ),
- ),
- const SizedBox(width: 12),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- file.name,
- style: const TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w600,
- fontFamily: 'GeneralSans',
- ),
+ child: DropdownButton<ProjectModel>(
+ value: _selectedEvent,
+ items:
+ _events
+ .map(
+ (e) => DropdownMenuItem(
+ value: e,
+ child: Text(
+ e.title,
+ style: const TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 14,
),
- const SizedBox(height: 4),
- Text(
- "Updated $dateStr",
- style: TextStyle(
- fontSize: 12,
- color: theme.colorScheme.onSurface.withOpacity(0.5),
- fontFamily: 'GeneralSans',
- ),
- ),
- ],
+ ),
),
- ),
- PopupMenuButton<String>(
- icon: const Icon(Icons.more_vert),
- onSelected: (value) {
- if (value == 'delete') {
- _deleteFile(file.id);
- } else if (value == 'open') {
- _openFile(file);
- }
- },
- itemBuilder:
- (context) => [
- const PopupMenuItem(value: 'open', child: Text("Open")),
- const PopupMenuItem(
- value: 'delete',
- child: Text(
- "Delete",
- style: TextStyle(color: Colors.red),
- ),
- ),
- ],
- ),
- ],
- ),
- if (file.description != null && file.description!.isNotEmpty) ...[
- const SizedBox(height: 12),
- Text(
- file.description!,
- style: TextStyle(
- fontSize: 13,
- color: theme.colorScheme.onSurface.withOpacity(0.7),
- fontFamily: 'GeneralSans',
- ),
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- ),
- ],
- if (file.tags.isNotEmpty) ...[
- const SizedBox(height: 12),
- Wrap(
- spacing: 8,
- children:
- file.tags
- .map(
- (tag) => Chip(
- label: Text(
- tag,
- style: const TextStyle(fontSize: 10),
- ),
- materialTapTargetSize:
- MaterialTapTargetSize.shrinkWrap,
- visualDensity: VisualDensity.compact,
- ),
- )
- .toList(),
- ),
- ],
- ],
- ),
+ )
+ .toList(),
+ onChanged: (e) {
+ if (e != null) _onSelectEvent(e);
+ },
+ underline: const SizedBox(),
+ isExpanded: true,
),
);
}
+
+ // ---------------------------------------
+ // FILTER
+ // ---------------------------------------
+ List<FileModel> _filteredAllFiles() {
+ if (_search.isEmpty) return _allFiles;
+ return _allFiles
+ .where(
+ (f) =>
+ f.name.toLowerCase().contains(_search.toLowerCase()) ||
+ _breadcrumbFor(f).toLowerCase().contains(_search.toLowerCase()),
+ )
+ .toList();
+ }
}
diff --git a/lib/ui/pages/share_to_file_page.dart b/lib/ui/pages/share_to_file_page.dart
@@ -1,12 +1,18 @@
import 'dart:io';
+import 'dart:convert';
+
import 'package:flutter/material.dart';
+import 'package:image/image.dart' as img;
+
import '../../services/file_service.dart';
+import '../../services/project_service.dart';
import '../../data/models/file_model.dart';
-import 'create_file_page.dart'; // Import the new creation page
+import '../../data/models/project_model.dart';
+import 'create_file_page.dart';
+import 'canvas_board_page.dart';
class ShareToFilePage extends StatefulWidget {
- final File sharedImage; // This is the image passed from ShareHandler
-
+ final File sharedImage;
const ShareToFilePage({super.key, required this.sharedImage});
@override
@@ -15,11 +21,20 @@ class ShareToFilePage extends StatefulWidget {
class _ShareToFilePageState extends State<ShareToFilePage> {
final FileService _fileService = FileService();
+ final ProjectService _projectService = ProjectService();
+
final TextEditingController _searchController = TextEditingController();
List<FileModel> _allFiles = [];
List<FileModel> _filteredFiles = [];
List<FileModel> _recentFiles = [];
+
+ // file.id -> { 'preview': path, 'dimensions': str }
+ Map<String, Map<String, String>> _fileMetadata = {};
+
+ // project cache to avoid repeated fetches
+ final Map<int, ProjectModel> _projectCache = {};
+
bool _isLoading = true;
String _searchQuery = "";
@@ -36,44 +51,159 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
}
Future<void> _fetchFiles() async {
+ setState(() => _isLoading = true);
try {
- setState(() => _isLoading = true);
-
- // Assuming projectId 0 is your 'Inbox' or default folder
- final files = await _fileService.getFiles(0);
+ // Using service-style API: fetch all files and recent files
+ // If your FileService has different method names, adapt them here.
+ final List<FileModel> files = await _fileService.getAllFiles();
+ final List<FileModel> recent = await _fileService.getRecentFiles(
+ limit: 10,
+ );
- // Sort by last updated to get recent files
+ // Sort by lastUpdated descending
files.sort((a, b) => b.lastUpdated.compareTo(a.lastUpdated));
+ recent.sort((a, b) => b.lastUpdated.compareTo(a.lastUpdated));
+
+ _allFiles = files;
+ _filteredFiles = List.from(files);
+ _recentFiles = recent.take(3).toList();
- setState(() {
- _allFiles = files;
- _filteredFiles = files;
- _recentFiles = files.take(3).toList(); // Get 3 most recent files
- _isLoading = false;
- });
+ // Load metadata for files (previews, dimensions)
+ await _loadFileMetadata(files);
+
+ setState(() => _isLoading = false);
} catch (e) {
- debugPrint("Error fetching files: $e");
+ debugPrint('Error fetching files in ShareToFilePage: $e');
setState(() => _isLoading = false);
}
}
+ Future<void> _loadFileMetadata(List<FileModel> files) async {
+ final Map<String, Map<String, String>> meta = {};
+
+ for (final fmodel in files) {
+ try {
+ final f = File(fmodel.filePath);
+ if (!await f.exists()) {
+ // skip if disk file missing
+ continue;
+ }
+
+ if (fmodel.filePath.toLowerCase().endsWith('.json')) {
+ // Canvas JSON - attempt to parse preview_path, width/height
+ try {
+ final content = await f.readAsString();
+ final data = jsonDecode(content);
+ String preview = '';
+ String dims = 'Unknown';
+
+ if (data is Map) {
+ if (data['preview_path'] != null &&
+ data['preview_path'].toString().isNotEmpty) {
+ preview = data['preview_path'].toString();
+ // If preview is relative, try to resolve relative to JSON file
+ if (!File(preview).existsSync()) {
+ final parentDir = f.parent.path;
+ final candidate = File('$parentDir/$preview');
+ if (candidate.existsSync()) preview = candidate.path;
+ }
+ }
+
+ if (data['width'] != null && data['height'] != null) {
+ final w = (data['width'] as num).toInt();
+ final h = (data['height'] as num).toInt();
+ dims = '$w x $h px';
+ }
+ }
+
+ meta[fmodel.id] = {'preview': preview, 'dimensions': dims};
+ } catch (e) {
+ debugPrint('Error parsing canvas json for ${fmodel.id}: $e');
+ }
+ } else {
+ // Regular image file - assign path and try to get dims
+ String dims = 'Unknown';
+ try {
+ final bytes = await f.readAsBytes();
+ final image = img.decodeImage(bytes);
+ if (image != null) dims = '${image.width} x ${image.height} px';
+ } catch (_) {
+ // ignore
+ }
+ meta[fmodel.id] = {'preview': fmodel.filePath, 'dimensions': dims};
+ }
+ } catch (e) {
+ debugPrint('Error while loading metadata for ${fmodel.id}: $e');
+ }
+ }
+
+ _fileMetadata = meta;
+ }
+
void _filterFiles(String query) {
setState(() {
_searchQuery = query;
- if (query.isEmpty) {
- _filteredFiles = _allFiles;
+ if (query.trim().isEmpty) {
+ _filteredFiles = List.from(_allFiles);
} else {
final q = query.toLowerCase();
_filteredFiles =
- _allFiles
- .where((file) => file.name.toLowerCase().contains(q))
- .toList();
+ _allFiles.where((file) {
+ final nameMatch = file.name.toLowerCase().contains(q);
+ final breadcrumb = _getProjectBreadcrumbSync(file).toLowerCase();
+ final projectMatch = breadcrumb.contains(q);
+ return nameMatch || projectMatch;
+ }).toList();
}
});
}
+ // Synchronous breadcrumb using cached project; returns empty if not cached
+ String _getProjectBreadcrumbSync(FileModel file) {
+ final proj = _projectCache[file.projectId];
+ if (proj == null) return '';
+ if (proj.parentId != null) {
+ final parent = _projectCache[proj.parentId!];
+ if (parent != null) return '${parent.title} / ${proj.title}';
+ }
+ return proj.title;
+ }
+
+ // Async breadcrumb loader that fetches projects into cache as needed
+ Future<String> _getProjectEventLabel(FileModel file) async {
+ // load file.projectId
+ if (!_projectCache.containsKey(file.projectId)) {
+ try {
+ final p = await _projectService.getProjectById(file.projectId);
+ if (p != null) _projectCache[file.projectId] = p;
+ } catch (e) {
+ debugPrint('Project load failed for ${file.projectId}: $e');
+ }
+ }
+
+ final project = _projectCache[file.projectId];
+ if (project == null) return "Unknown";
+
+ if (project.parentId == null) {
+ return project.title;
+ }
+
+ final parentId = project.parentId!;
+ if (!_projectCache.containsKey(parentId)) {
+ try {
+ final parent = await _projectService.getProjectById(parentId);
+ if (parent != null) _projectCache[parentId] = parent;
+ } catch (e) {
+ debugPrint('Parent project load failed for $parentId: $e');
+ }
+ }
+
+ final parentProject = _projectCache[parentId];
+ if (parentProject == null) return project.title;
+ return "${parentProject.title} / ${project.title}";
+ }
+
void _onAddPressed() {
- // Redirect to the CreateFilePage (Canvas Selection) with the shared image
Navigator.push(
context,
MaterialPageRoute(
@@ -82,16 +212,90 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
);
}
- void _onFileSelected(FileModel file) {
- // TODO: Handle file selection - maybe open editor with this file
- // For now, just show a snackbar
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text('Selected: ${file.name}')));
+ void _onFileSelected(FileModel file) async {
+ try {
+ final f = File(file.filePath);
+ if (!await f.exists()) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text("File not found")));
+ return;
+ }
+
+ double width = 1080;
+ double height = 1080;
+
+ if (file.filePath.toLowerCase().endsWith('.json')) {
+ final content = await f.readAsString();
+ final data = jsonDecode(content);
+
+ if (data is Map) {
+ if (data['width'] != null && data['height'] != null) {
+ width = (data['width'] as num).toDouble();
+ height = (data['height'] as num).toDouble();
+ }
+ }
+ }
+
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder:
+ (context) => CanvasBoardPage(
+ projectId: file.projectId,
+ width: width,
+ height: height,
+ existingFile: file,
+ injectedMedia: widget.sharedImage, // 🔥 THIS IS THE FIX
+ ),
+ ),
+ );
+ } catch (e) {
+ debugPrint("Error opening file: $e");
+ }
+ }
+
+
+
+ String _formatDate(DateTime date) {
+ final now = DateTime.now();
+ final diff = now.difference(date);
+ if (diff.inDays == 0) return 'Today';
+ if (diff.inDays == 1) return 'Yesterday';
+ if (diff.inDays < 7) return '${diff.inDays} days ago';
+ return '${date.day}/${date.month}/${date.year}';
+ }
+
+ // Resolve preview path; if empty or missing, fallback to original filePath
+ String _resolvePreviewPath(FileModel file) {
+ final meta = _fileMetadata[file.id];
+ if (meta == null) return file.filePath;
+ final preview = meta['preview'] ?? '';
+ if (preview.isNotEmpty && File(preview).existsSync()) return preview;
+ // fallback: if file is json and has no preview, return placeholder or file.path
+ if (file.filePath.toLowerCase().endsWith('.json')) {
+ // attempt to find PNG/JPG sibling in same folder with same base name
+ final f = File(file.filePath);
+ final base = f.uri.pathSegments.last;
+ final nameWithoutExt = base.split('.').first;
+ final parent = f.parent;
+ final candidates = [
+ '${parent.path}/$nameWithoutExt.png',
+ '${parent.path}/$nameWithoutExt.jpg',
+ '${parent.path}/preview_$nameWithoutExt.png',
+ ];
+ for (final c in candidates) {
+ if (File(c).existsSync()) return c;
+ }
+ }
+ if (File(file.filePath).existsSync()) return file.filePath;
+ return '';
}
@override
Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
@@ -102,7 +306,7 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
onPressed: () => Navigator.pop(context),
),
title: const Text(
- 'Your Files',
+ 'Files',
style: TextStyle(
color: Color(0xFF27272A),
fontFamily: 'GeneralSans',
@@ -126,7 +330,7 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
? _buildEmptyState()
: Column(
children: [
- // --- Search Bar ---
+ // Search bar
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: SizedBox(
@@ -175,7 +379,7 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
padding: EdgeInsets.zero,
onPressed: () {
_searchController.clear();
- _filterFiles("");
+ _filterFiles('');
},
)
: null,
@@ -189,9 +393,9 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- // --- RECENT FILES SECTION ---
- if (_searchQuery.isEmpty &&
- _recentFiles.isNotEmpty) ...[
+ // Recent Files (UI like screenshot)
+ if (_recentFiles.isNotEmpty &&
+ _searchQuery.isEmpty) ...[
const Padding(
padding: EdgeInsets.symmetric(
horizontal: 20,
@@ -224,7 +428,7 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
const SizedBox(height: 24),
],
- // --- ALL FILES SECTION ---
+ // All files header
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
@@ -243,6 +447,8 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
),
),
),
+
+ // All files list
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: ListView.builder(
@@ -286,6 +492,8 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
}
Widget _buildRecentFileItem(FileModel file) {
+ final previewPath = _resolvePreviewPath(file);
+
return Container(
margin: const EdgeInsets.only(bottom: 8),
child: InkWell(
@@ -300,42 +508,66 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
),
child: Row(
children: [
- // File Thumbnail
- Container(
- width: 56,
- height: 56,
- decoration: BoxDecoration(
- color: const Color(0xFFFAFAFA),
- borderRadius: BorderRadius.circular(8),
- ),
- child: ClipRRect(
- borderRadius: BorderRadius.circular(8),
- child:
- File(file.filePath).existsSync()
- ? Image.file(
- File(file.filePath),
- fit: BoxFit.cover,
- errorBuilder:
- (_, __, ___) => Icon(
- Icons.image,
- color: Colors.grey[400],
- size: 28,
- ),
- )
- : Icon(
- Icons.image,
- color: Colors.grey[400],
- size: 28,
- ),
+ // thumbnail
+ SizedBox(
+ width: 72,
+ height: 72,
+ child: Center(
+ child: Container(
+ width: 66,
+ height: 66,
+ decoration: BoxDecoration(
+ color: Colors.grey[200],
+ borderRadius: BorderRadius.circular(8),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.08),
+ blurRadius: 6,
+ offset: Offset(0, 3),
+ ),
+ ],
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child:
+ previewPath.isNotEmpty &&
+ File(previewPath).existsSync()
+ ? Image.file(
+ File(previewPath),
+ fit: BoxFit.cover,
+ errorBuilder: (c, e, s) => _placeholderIcon(),
+ )
+ : _placeholderIcon(),
+ ),
+ ),
),
),
- const SizedBox(width: 10),
- // File Info
+
+ const SizedBox(width: 12),
+
+ // info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisAlignment: MainAxisAlignment.center,
children: [
+ FutureBuilder<String>(
+ future: _getProjectEventLabel(file),
+ builder: (context, snapshot) {
+ final label = snapshot.data ?? "";
+ return Text(
+ label,
+ style: const TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 12,
+ fontWeight: FontWeight.w400,
+ color: Color(0xFF71717B),
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ );
+ },
+ ),
+ const SizedBox(height: 6),
Text(
file.name,
style: const TextStyle(
@@ -347,20 +579,18 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
- const SizedBox(height: 2),
+ const SizedBox(height: 6),
Text(
_formatDate(file.lastUpdated),
style: const TextStyle(
fontFamily: 'GeneralSans',
fontSize: 12,
color: Color(0xFF71717B),
- fontWeight: FontWeight.w400,
),
),
],
),
),
- const SizedBox(width: 12),
],
),
),
@@ -369,6 +599,8 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
}
Widget _buildFileItem(FileModel file) {
+ final previewPath = _resolvePreviewPath(file);
+
return Container(
margin: const EdgeInsets.only(bottom: 8),
child: InkWell(
@@ -383,42 +615,64 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
),
child: Row(
children: [
- // File Thumbnail
- Container(
- width: 48,
- height: 48,
- decoration: BoxDecoration(
- color: const Color(0xFFFAFAFA),
- borderRadius: BorderRadius.circular(8),
- ),
- child: ClipRRect(
- borderRadius: BorderRadius.circular(8),
- child:
- File(file.filePath).existsSync()
- ? Image.file(
- File(file.filePath),
- fit: BoxFit.cover,
- errorBuilder:
- (_, __, ___) => Icon(
- Icons.description,
- color: Colors.grey[400],
- size: 24,
- ),
- )
- : Icon(
- Icons.description,
- color: Colors.grey[400],
- size: 24,
- ),
+ SizedBox(
+ width: 72,
+ height: 72,
+ child: Center(
+ child: Container(
+ width: 66,
+ height: 66,
+ decoration: BoxDecoration(
+ color: Colors.grey[200],
+ borderRadius: BorderRadius.circular(8),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.08),
+ blurRadius: 6,
+ offset: Offset(0, 3),
+ ),
+ ],
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child:
+ previewPath.isNotEmpty &&
+ File(previewPath).existsSync()
+ ? Image.file(
+ File(previewPath),
+ fit: BoxFit.cover,
+ errorBuilder: (c, e, s) => _placeholderIcon(),
+ )
+ : _placeholderIcon(),
+ ),
+ ),
),
),
+
const SizedBox(width: 12),
- // File Info
+
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisAlignment: MainAxisAlignment.center,
children: [
+ FutureBuilder<String>(
+ future: _getProjectEventLabel(file),
+ builder: (context, snapshot) {
+ final label = snapshot.data ?? "";
+ return Text(
+ label,
+ style: const TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 12,
+ fontWeight: FontWeight.w400,
+ color: Color(0xFF71717B),
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ );
+ },
+ ),
+ const SizedBox(height: 4),
Text(
file.name,
style: const TextStyle(
@@ -430,20 +684,18 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
- const SizedBox(height: 2),
+ const SizedBox(height: 4),
Text(
_formatDate(file.lastUpdated),
style: const TextStyle(
fontFamily: 'GeneralSans',
fontSize: 12,
color: Color(0xFF71717B),
- fontWeight: FontWeight.w400,
),
),
],
),
),
- const SizedBox(width: 12),
],
),
),
@@ -451,18 +703,10 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
);
}
- String _formatDate(DateTime date) {
- final now = DateTime.now();
- final difference = now.difference(date);
-
- if (difference.inDays == 0) {
- return "Today";
- } else if (difference.inDays == 1) {
- return "Yesterday";
- } else if (difference.inDays < 7) {
- return "${difference.inDays} days ago";
- } else {
- return "${date.day}/${date.month}/${date.year}";
- }
+ Widget _placeholderIcon() {
+ return Container(
+ color: Colors.grey[200],
+ child: Icon(Icons.image, size: 28, color: Colors.grey[400]),
+ );
}
}