@@ -27,10 +27,17 @@ Smart File Manager (SFM) is a digital housekeeper that uses metadata, natural la
- [🌳 Branching Strategy](#-branching-strategy)
- [✔️ Quality Badges](#️-quality-badges)
+## 📩 Get our installer here
+- [link to our installer](https://drive.google.com/drive/folders/1KlQ3yYhmHYFbv0vVpLBJv5CjVyD5idO4?usp=drive_link)
+
## 🎥 Demo Videos
- [Demo 1 Video](https://drive.google.com/file/d/13_amEyoW3bTHRSY3giBA2xw_4UlJE3Eq/view?usp=sharing)
+- [Demo 2 Video](https://drive.google.com/file/d/1r3b0Cgn8EPdx442N7hdfmTc4ODt7TX4g/view?usp=drive_link)
+
+- [Demo 3 Video]()
+
## 📑 Documentation
+
## 📊 Project Board
The GitHub Project boards are used to track current tasks allocated to each team member, facilitating effective communication, progress monitoring, and ensuring we meet project deadlines while maintaining accountability and transparency throughout the development process.
@@ -74,7 +95,7 @@ The GitHub Project boards are used to track current tasks allocated to each team
Dewald Colesky
-
+
|
Backend Legend
diff --git a/app/.gitignore b/app/.gitignore
index 79c113f..691feee 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -43,3 +43,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
+
+pubspec.lock
+
diff --git a/app/assets/logo.ico b/app/assets/logo.ico
new file mode 100644
index 0000000..74f0227
Binary files /dev/null and b/app/assets/logo.ico differ
diff --git a/app/lib/api.dart b/app/lib/api.dart
index 3b7156e..731d629 100644
--- a/app/lib/api.dart
+++ b/app/lib/api.dart
@@ -1,133 +1,476 @@
-import 'package:http/http.dart' as http;
-import 'dart:convert';
-import 'models/file_tree_node.dart';
-
-const uri = "http://localhost:51000";
-
-class Api {
- //Call to load tree data
- static Future loadTreeData(String name) async {
- try {
- final response = await http.get(
- Uri.parse("$uri/loadTreeData?name=$name"),
- );
- print(response.body);
-
- if (response.statusCode == 200) {
- return FileTreeNode.fromJson(
- jsonDecode(response.body) as Map,
- );
- } else {
- throw Exception('Failed to load data: HTTP ${response.statusCode}');
- }
- } catch (e, stackTrace) {
- print('Error loading tree data from loadTreeData: $e');
- print(stackTrace);
- rethrow;
- }
- }
-
- //Call To Sort Tree structure
- static Future sortManager(String name) async {
- try {
- final response = await http.get(Uri.parse("$uri/sortTree?name=$name"));
- print(response.body);
-
- if (response.statusCode == 200) {
- return FileTreeNode.fromJson(
- jsonDecode(response.body) as Map,
- );
- } else {
- throw Exception('Failed to load data: HTTP ${response.statusCode}');
- }
- } catch (e, stackTrace) {
- print('Error sorting tree structure from sortManager: $e');
- print(stackTrace);
- rethrow;
- }
- }
-
- //Call to add Mangager to backend
- static Future addSmartManager(String name, String path) async {
- try {
- final response = await http.post(
- Uri.parse("$uri/addDirectory?name=$name&path=$path"),
- );
- print("$uri/addDirectory?name=$name&path=$path");
- if (response.statusCode == 200 || response.statusCode == 201) {
- return true;
- } else {
- throw Exception(
- 'Failed to add SmartManager: HTTP ${response.statusCode}',
- );
- }
- } catch (e, stackTrace) {
- print('Error creating smart manager at addDirectory: $e');
- print(stackTrace);
- rethrow;
- }
- }
-
- //Call to delete Mangager to backend
- static Future deleteSmartManager(String name) async {
- try {
- final response = await http.post(
- Uri.parse("$uri/deleteDirectory?name=$name"),
- );
-
- if (response.statusCode == 200 || response.statusCode == 201) {
- return true;
- } else {
- throw Exception(
- 'Failed to delete SmartManager: HTTP ${response.statusCode}',
- );
- }
- } catch (e, stackTrace) {
- print('Error deleting smart manager at deleteDirectory: $e');
- print(stackTrace);
- rethrow;
- }
- }
-
- //Call to add tag to File
- static Future addTagToFile(String name, String path, String tag) async {
- try {
- final response = await http.post(
- Uri.parse("$uri/addTag?name=$name&path=$path&tag=$tag"),
- );
-
- if (response.statusCode == 200 || response.statusCode == 201) {
- return true;
- } else {
- throw Exception('Failed to add Tag: HTTP ${response.statusCode}');
- }
- } catch (e, stackTrace) {
- print('Error adding tag at addTag deleteDirectory: $e');
- print(stackTrace);
- rethrow;
- }
- }
-
- //Call to delete tag from File
- static Future deleteTagFromFile(
- String name,
- String path,
- String tag,
- ) async {
- try {
- final response = await http.post(
- Uri.parse("$uri/removeTag?name=$name&path=$path&tag=$tag"),
- );
-
- if (response.statusCode == 200 || response.statusCode == 201) {
- return true;
- } else {
- throw Exception('Failed to delete Tag: HTTP ${response.statusCode}');
- }
- } catch (e, stackTrace) {
- print('Error deleting tag at deleteTag: $e');
- print(stackTrace);
- rethrow;
- }
- }
-}
+import 'package:app/models/file_model.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+import 'models/file_tree_node.dart';
+import 'models/startup_response.dart';
+import 'models/duplicate_model.dart';
+import 'models/stats_model.dart';
+
+const uri = "http://localhost:51000";
+
+class Api {
+ //Call to load tree data
+ static Future loadTreeData(String name) async {
+ try {
+ final response = await http.get(
+ Uri.parse("$uri/loadTreeData?name=$name"),
+ );
+
+ if (response.statusCode == 200) {
+ return FileTreeNode.fromJson(
+ jsonDecode(response.body) as Map,
+ );
+ } else {
+ throw Exception('Failed to load data: HTTP ${response.statusCode}');
+ }
+ } catch (e, stackTrace) {
+ print('Error loading tree data from loadTreeData: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //Call to initialize app and get existing smart managers
+ static Future startUp() async {
+ try {
+ final response = await http.get(Uri.parse("$uri/startUp"));
+
+ if (response.statusCode == 200) {
+ return StartupResponse.fromJson(
+ jsonDecode(response.body) as Map,
+ );
+ } else {
+ throw Exception('Failed to load data: HTTP ${response.statusCode}');
+ }
+ } catch (e, stackTrace) {
+ print('Error loading initial data from startup: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //Call To Sort Tree structure
+ static Future sortManager(String name) async {
+ try {
+ final response = await http.get(Uri.parse("$uri/sortTree?name=$name"));
+
+ if (response.statusCode == 200) {
+ return FileTreeNode.fromJson(
+ jsonDecode(response.body) as Map,
+ );
+ } else {
+ throw Exception('Failed to load data: HTTP ${response.statusCode}');
+ }
+ } catch (e, stackTrace) {
+ print('Error sorting tree structure from sortManager: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //Call to add Mangager to backend
+ static Future addSmartManager(String name, String path) async {
+ try {
+ final response = await http.post(
+ Uri.parse("$uri/addDirectory?name=$name&path=$path"),
+ );
+ if (response.statusCode == 200 || response.statusCode == 201) {
+ return true;
+ } else {
+ throw Exception(
+ 'Failed to add SmartManager: HTTP ${response.statusCode}',
+ );
+ }
+ } catch (e, stackTrace) {
+ print('Error creating smart manager at addDirectory: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //Call to delete Mangager to backend
+ static Future deleteSmartManager(String name) async {
+ try {
+ final response = await http.post(
+ Uri.parse("$uri/deleteManager?name=$name"),
+ );
+
+ if (response.statusCode == 200 || response.statusCode == 201) {
+ print(response.body);
+ return true;
+ } else {
+ throw Exception(
+ 'Failed to delete SmartManager: HTTP ${response.statusCode}',
+ );
+ }
+ } catch (e, stackTrace) {
+ print('Error deleting smart manager at deleteDirectory: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //Call to add tag to File
+ static Future addTagToFile(String name, String path, String tag) async {
+ try {
+ final response = await http.post(
+ Uri.parse("$uri/addTag?name=$name&path=$path&tag=$tag"),
+ );
+
+ if (response.statusCode == 200 || response.statusCode == 201) {
+ return true;
+ } else {
+ throw Exception('Failed to add Tag: HTTP ${response.statusCode}');
+ }
+ } catch (e, stackTrace) {
+ print('Error adding tag at addTag deleteDirectory: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //Call to delete tag from File
+ static Future deleteTagFromFile(
+ String name,
+ String path,
+ String tag,
+ ) async {
+ try {
+ final response = await http.post(
+ Uri.parse("$uri/removeTag?name=$name&path=$path&tag=$tag"),
+ );
+
+ if (response.statusCode == 200 || response.statusCode == 201) {
+ return true;
+ } else {
+ throw Exception('Failed to delete Tag: HTTP ${response.statusCode}');
+ }
+ } catch (e, stackTrace) {
+ print('Error deleting tag at deleteTag: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //lock files/folders
+ static Future locking(String managerName, String path) async {
+ try {
+ final response = await http.post(
+ Uri.parse("$uri/lock?name=$managerName&path=$path"),
+ );
+ if (response.statusCode == 200 || response.statusCode == 201) {
+ return true;
+ } else {
+ throw Exception(
+ 'Failed to lock File/Folder: HTTP ${response.statusCode}',
+ );
+ }
+ } catch (e, stackTrace) {
+ print('Error locking folder/file: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ static Future unlocking(String managerName, String path) async {
+ try {
+ final response = await http.post(
+ Uri.parse("$uri/unlock?name=$managerName&path=$path"),
+ );
+ if (response.statusCode == 200 || response.statusCode == 201) {
+ return true;
+ } else {
+ throw Exception(
+ 'Failed to unlock File/Folder: HTTP ${response.statusCode}',
+ );
+ }
+ } catch (e, stackTrace) {
+ print('Error unlocking folder/file: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //Call to load duplicate data
+ static Future> loadDuplicates(String name) async {
+ try {
+ final response = await http.get(
+ Uri.parse("$uri/findDuplicateFiles?name=$name"),
+ );
+
+ if (response.statusCode == 200) {
+ final List jsonList = jsonDecode(response.body);
+ return jsonList
+ .map(
+ (item) => DuplicateModel.fromJson(item as Map),
+ )
+ .toList();
+ } else {
+ throw Exception(
+ 'Failed to load duplicate data: HTTP ${response.statusCode}',
+ );
+ }
+ } catch (e, stackTrace) {
+ print('Error loading duplicates: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //Endpoint to use GO search(Simple file name search)
+ static Future searchGo(String name, String searchString) async {
+ try {
+ final response = await http.get(
+ Uri.parse("$uri/search?compositeName=$name&searchText=$searchString"),
+ );
+
+ if (response.statusCode == 200) {
+ print("Normal Search Used");
+
+ return FileTreeNode.fromJson(
+ jsonDecode(response.body) as Map,
+ );
+ } else {
+ throw Exception('Failed to load data: HTTP ${response.statusCode}');
+ }
+ } catch (e, stackTrace) {
+ print('Error loading tree data from loadTreeData: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //Endpoint to use Advanced search (keywords)
+ static Future searchAdvanced(
+ String name,
+ String searchString,
+ ) async {
+ try {
+ final response = await http.get(
+ Uri.parse(
+ "$uri/keywordSearch?compositeName=$name&searchText=$searchString",
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ print("Advanced Search Used");
+ return FileTreeNode.fromJson(
+ jsonDecode(response.body) as Map,
+ );
+ } else {
+ throw Exception('Failed to load data: HTTP ${response.statusCode}');
+ }
+ } catch (e, stackTrace) {
+ print('Error loading tree data from loadTreeData: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //return true/false depending if advanced search is ready
+ static Future searchAdvancedReady(String name) async {
+ try {
+ final response = await http.get(
+ Uri.parse("$uri/isKeywordSearchReady?compositeName=$name"),
+ );
+ if (response.statusCode == 200) {
+ final body = response.body.trim().toLowerCase();
+ return body == "true" || body == "1";
+ } else {
+ throw Exception('Failed to load data: HTTP ${response.statusCode}');
+ }
+ } catch (e, stackTrace) {
+ print('Error loading tree data from loadTreeData: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ static Future deleteSingleFile(
+ String managerName,
+ String path,
+ ) async {
+ try {
+ final response = await http.post(
+ Uri.parse("$uri/deleteFile?name=$managerName&path=$path"),
+ );
+ if (response.statusCode == 200 || response.statusCode == 201) {
+ return FileTreeNode.fromJson(
+ jsonDecode(response.body) as Map,
+ );
+ } else {
+ throw Exception('Failed to delete File: HTTP ${response.statusCode}');
+ }
+ } catch (e, stackTrace) {
+ print('Error deleting file: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ static Future bulkDeleteFiles(
+ String managerName,
+ String jsonPaths,
+ ) async {
+ try {
+ final response = await http.post(
+ Uri.parse("$uri/bulkDeleteFiles?name=$managerName"),
+ headers: {'Content-Type': 'application/json'},
+ body: jsonPaths,
+ );
+ if (response.statusCode == 200 || response.statusCode == 201) {
+ return FileTreeNode.fromJson(
+ jsonDecode(response.body) as Map,
+ );
+ } else {
+ throw Exception('Failed to delete files: HTTP ${response.statusCode}');
+ }
+ } catch (e, stackTrace) {
+ print('Error deleting files: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //Bulk add Tag
+ static Future bulkAddTag(
+ String managerName,
+ String jsonPaths,
+ ) async {
+ try {
+ final response = await http.post(
+ Uri.parse("$uri/bulkAddTag?name=$managerName"),
+ headers: {'Content-Type': 'application/json'},
+ body: jsonPaths,
+ );
+ if (response.statusCode == 200 || response.statusCode == 201) {
+ return FileTreeNode.fromJson(
+ jsonDecode(response.body) as Map,
+ );
+ } else {
+ throw Exception('Failed to delete files: HTTP ${response.statusCode}');
+ }
+ } catch (e, stackTrace) {
+ print('Error deleting files: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //Bulk remove Tag
+ static Future bulkRemoveTag(
+ String managerName,
+ String jsonPaths,
+ ) async {
+ try {
+ final response = await http.post(
+ Uri.parse("$uri/bulkRemoveTag?name=$managerName"),
+ headers: {'Content-Type': 'application/json'},
+ body: jsonPaths,
+ );
+ if (response.statusCode == 200 || response.statusCode == 201) {
+ return FileTreeNode.fromJson(
+ jsonDecode(response.body) as Map,
+ );
+ } else {
+ throw Exception('Failed to remove tags: HTTP ${response.statusCode}');
+ }
+ } catch (e, stackTrace) {
+ print('Error removing tags: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ static Future> bulkOperation(
+ String name,
+ String type,
+ bool umbrella,
+ ) async {
+ try {
+ final response = await http.get(
+ Uri.parse("$uri/returnType?name=$name&type=$type&umbrella=$umbrella"),
+ );
+
+ if (response.statusCode == 200) {
+ if (response.body == "" || response.body == "null") {
+ return [];
+ }
+ try {
+ final dynamic jsonData = jsonDecode(response.body);
+ print(jsonData);
+
+ if (jsonData == null) {
+ return [];
+ }
+
+ if (jsonData is! List) {
+ return [];
+ }
+
+ final List jsonList = jsonData;
+ return jsonList
+ .map((item) => FileModel.fromJson(item as Map))
+ .toList();
+ } catch (e) {
+ print('Error parsing JSON: $e');
+ return [];
+ }
+ } else {
+ throw Exception(
+ 'Failed to load duplicate data: HTTP ${response.statusCode}',
+ );
+ }
+ } catch (e, stackTrace) {
+ print('Error loading duplicates: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //loadStats
+ static Future loadStatsData() async {
+ try {
+ final response = await http.get(Uri.parse("$uri/returnStats"));
+
+ if (response.statusCode == 200) {
+ print(jsonDecode(response.body) as List);
+
+ return ManagersStatsResponse.fromJson(
+ jsonDecode(response.body) as List,
+ );
+ } else {
+ throw Exception('Failed to load stats: HTTP ${response.statusCode}');
+ }
+ } catch (e, stackTrace) {
+ print('Error loading stats data: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+
+ //Move directory
+ static Future moveDirectory(String managerName) async {
+ try {
+ final response = await http.post(
+ Uri.parse("$uri/moveDirectory?name=$managerName"),
+ );
+ if (response.statusCode == 200 || response.statusCode == 201) {
+ print(response.body);
+ if (response.body == "true") {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ throw Exception(
+ 'Failed to unlock File/Folder: HTTP ${response.statusCode}',
+ );
+ }
+ } catch (e, stackTrace) {
+ print('Error unlocking folder/file: $e');
+ print(stackTrace);
+ rethrow;
+ }
+ }
+}
diff --git a/app/lib/constants.dart b/app/lib/constants.dart
index f8de5e9..d2ed290 100644
--- a/app/lib/constants.dart
+++ b/app/lib/constants.dart
@@ -11,3 +11,9 @@ const kTitle1 = TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
);
+
+const kTitle2 = TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ fontSize: 20,
+);
diff --git a/app/lib/custom_widgets/bulk_dialog.dart b/app/lib/custom_widgets/bulk_dialog.dart
new file mode 100644
index 0000000..bbd459f
--- /dev/null
+++ b/app/lib/custom_widgets/bulk_dialog.dart
@@ -0,0 +1,988 @@
+import 'dart:convert';
+import 'package:app/custom_widgets/custom_dropdown_menu.dart';
+import 'package:app/models/file_tree_node.dart';
+import 'package:flutter/material.dart';
+import 'package:app/constants.dart';
+import 'package:app/api.dart';
+import 'package:app/models/file_model.dart';
+
+class BulkDialog extends StatefulWidget {
+ final String name;
+ final String type;
+ final bool umbrella;
+ List? files;
+ final Function(String, FileTreeNode) updateOnDelete;
+
+ BulkDialog({
+ required this.name,
+ required this.type,
+ required this.umbrella,
+ required this.updateOnDelete,
+ this.files,
+ super.key,
+ });
+
+ @override
+ State createState() => _BulkDialogState();
+}
+
+class _BulkDialogState extends State {
+ final TextEditingController _bulkController = TextEditingController();
+ bool _isLoading = true;
+ final List _currentSelectedFiles = [];
+ bool _selectAll = false;
+
+ String _selectedCategory = "All Files";
+ String? _selectedFileType;
+
+ String _selectedBulkOperation = "Bulk Delete";
+ String? _selectedTagFilter;
+
+ final List _bulkOperations = [
+ "Bulk Delete",
+ "Bulk Add Tag",
+ "Bulk Remove Tag",
+ ];
+
+ final Map> _fileTypeMap = {
+ "All Files": [],
+ "Documents": ["pdf", "doc", "docx", "rtf", "txt", "odt", "md", "csv"],
+ "Images": [
+ "jpg",
+ "jpeg",
+ "png",
+ "gif",
+ "bmp",
+ "tiff",
+ "tif",
+ "webp",
+ "svg",
+ ],
+ "Music": ["mp3", "wav", "flac", "aac", "m4a", "ogg", "wma"],
+ "Presentations": ["ppt", "pptx", "odp", "key"],
+ "Videos": ["mp4", "mkv", "avi", "mov", "wmv", "webm"],
+ "Spreadsheets": ["xls", "xlsx", "ods", "tsv", "xlsm"],
+ "Archives": ["zip", "rar", "7z", "tar", "gz", "iso"],
+ "Other": [],
+ };
+
+ @override
+ void dispose() {
+ _bulkController.dispose();
+ super.dispose();
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ if (_selectedBulkOperation == "Bulk Remove Tag") {
+ _loadBulkData(widget.name, "TAGS", widget.umbrella);
+ } else {
+ String apiType =
+ _selectedCategory == "All Files" ? "ALL" : _selectedCategory;
+ _loadBulkData(widget.name, apiType, widget.umbrella);
+ }
+ }
+
+ Future _loadBulkData(String name, String type, bool umbrella) async {
+ setState(() {
+ _isLoading = true;
+ });
+
+ final files = await Api.bulkOperation(name, type, umbrella);
+
+ if (mounted) {
+ setState(() {
+ widget.files = files ?? [];
+ _currentSelectedFiles.clear();
+ _selectAll = false;
+ _isLoading = false;
+ });
+ }
+ }
+
+ void _onCategoryChanged(String? newCategory) {
+ if (newCategory != null && newCategory != _selectedCategory) {
+ setState(() {
+ _selectedCategory = newCategory;
+ _selectedFileType = null;
+ });
+ String apiType =
+ _selectedCategory == "All Files" ? "ALL" : _selectedCategory;
+ _loadBulkData(widget.name, apiType, true);
+ }
+ }
+
+ void _onBulkOperationChanged(String? newBulkOperation) {
+ if (newBulkOperation != null &&
+ newBulkOperation != _selectedBulkOperation) {
+ setState(() {
+ _selectedCategory = "All Files";
+ _selectedFileType = null;
+ _selectedBulkOperation = newBulkOperation;
+ _selectedTagFilter = null;
+ });
+ if (newBulkOperation == "Bulk Remove Tag") {
+ _loadBulkData(widget.name, "TAGS", true);
+ } else {
+ String apiType =
+ _selectedCategory == "All Files" ? "ALL" : _selectedCategory;
+ _loadBulkData(widget.name, apiType, true);
+ }
+ }
+ }
+
+ void _onFileTypeChanged(String? newFileType) {
+ if (newFileType != _selectedFileType) {
+ setState(() {
+ _selectedFileType = newFileType;
+ });
+ if (newFileType != null) {
+ _loadBulkData(widget.name, newFileType, false);
+ } else {
+ String apiType =
+ _selectedCategory == "All Files" ? "ALL" : _selectedCategory;
+ _loadBulkData(widget.name, apiType, true);
+ }
+ }
+ }
+
+ void _onTagFilterChanged(String? newTag) {
+ setState(() {
+ _selectedTagFilter = newTag;
+ });
+ }
+
+ List _getAllAvailableTags() {
+ Set allTags = {};
+ if (widget.files != null) {
+ for (FileModel file in widget.files!) {
+ if (file.fileTags != null) {
+ allTags.addAll(file.fileTags!);
+ }
+ }
+ }
+ return allTags.toList()..sort();
+ }
+
+ List _getTagsFromSelectedFiles() {
+ Set selectedFileTags = {};
+ for (FileModel file in _currentSelectedFiles) {
+ if (file.fileTags != null) {
+ selectedFileTags.addAll(file.fileTags!);
+ }
+ }
+ return selectedFileTags.toList()..sort();
+ }
+
+ void _toggleSelectAll() {
+ setState(() {
+ _selectAll = !_selectAll;
+ _currentSelectedFiles.clear();
+ if (_selectAll && widget.files != null) {
+ // Filter files based on bulk operation and tag filter before adding to selection
+ List filesToAdd =
+ widget.files!.where((file) {
+ if (_selectedBulkOperation == "Bulk Remove Tag") {
+ if (file.fileTags == null || file.fileTags!.isEmpty) {
+ return false;
+ }
+ if (_selectedTagFilter != null) {
+ return file.fileTags!.contains(_selectedTagFilter);
+ }
+ return true;
+ }
+ return true;
+ }).toList();
+ _currentSelectedFiles.addAll(filesToAdd);
+ }
+ });
+ }
+
+ void _toggleFileSelection(FileModel file) {
+ setState(() {
+ if (_currentSelectedFiles.contains(file)) {
+ _currentSelectedFiles.remove(file);
+ } else {
+ _currentSelectedFiles.add(file);
+ }
+ // Calculate the total number of available files based on filter
+ int totalAvailableFiles = 0;
+ if (widget.files != null) {
+ totalAvailableFiles =
+ widget.files!.where((file) {
+ if (_selectedBulkOperation == "Bulk Remove Tag") {
+ if (file.fileTags == null || file.fileTags!.isEmpty) {
+ return false;
+ }
+ if (_selectedTagFilter != null) {
+ return file.fileTags!.contains(_selectedTagFilter);
+ }
+ return true;
+ }
+ return true;
+ }).length;
+ }
+ _selectAll = _currentSelectedFiles.length == totalAvailableFiles;
+ });
+ }
+
+ String _convertToJsonDelete() {
+ List |