diff --git a/README.md b/README.md index 5a6a6e3..801fbc0 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,21 @@ A small and simple Tool to send UDP packages over custom ports to control logic in Unity projects. +## Storage + +All projects and packages are stored in a **JSON file** located in your Documents folder: +- **Windows**: `C:\Users\\Documents\unityudp_projects.json` +- **macOS**: `~/Documents/unityudp_projects.json` +- **Linux**: `~/Documents/unityudp_projects.json` + +This makes it easy to: +- 📤 **Share** projects with other users +- 💾 **Backup** your configurations +- 📝 **Edit** manually if needed (JSON format) +- 🔄 **Version control** with Git + +Click the folder icon in the app to see the exact file location on your system. + ## Getting Started ### Prerequisites diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index b10a34c..0346c02 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -90,6 +90,53 @@ class _HomeScreenState extends State { ); } + Future _showStorageInfo() async { + final filePath = await _storageService.getProjectsFilePath(); + if (!mounted) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Row( + children: [ + Icon(Icons.folder_open), + SizedBox(width: 8), + Text('Storage Location'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Your projects are saved in:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + SelectableText( + filePath, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + ), + ), + const SizedBox(height: 16), + const Text( + 'You can share this file with other users or back it up for safekeeping.', + style: TextStyle(fontSize: 12), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + } + void _showWebWarning() { showDialog( context: context, @@ -137,6 +184,12 @@ class _HomeScreenState extends State { tooltip: 'Web Platform Limitations', ), ), + if (!kIsWeb) + IconButton( + icon: const Icon(Icons.folder), + onPressed: _showStorageInfo, + tooltip: 'Storage Location', + ), IconButton( icon: const Icon(Icons.info_outline), onPressed: _showAboutDialog, diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index b9bb1e5..638f1d9 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -1,22 +1,27 @@ import 'dart:convert'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; import '../models/project.dart'; class StorageService { - static const String _projectsKey = 'projects'; - static const String _currentPortKey = 'current_port'; + static const String _projectsFileName = 'unityudp_projects.json'; + + Future get _localPath async { + final directory = await getApplicationDocumentsDirectory(); + return directory.path; + } + + Future get _projectsFile async { + final path = await _localPath; + return File('$path/$_projectsFileName'); + } Future saveProjects(List projects) async { try { - final prefs = await SharedPreferences.getInstance(); + final file = await _projectsFile; final jsonList = projects.map((p) => p.toJson()).toList(); - final jsonString = json.encode(jsonList); - final success = await prefs.setString(_projectsKey, jsonString); - if (!success) { - throw Exception('Failed to save projects to storage'); - } - // Force a commit on web - await prefs.reload(); + final jsonString = const JsonEncoder.withIndent(' ').convert(jsonList); + await file.writeAsString(jsonString); } catch (e) { rethrow; } @@ -24,31 +29,60 @@ class StorageService { Future> loadProjects() async { try { - final prefs = await SharedPreferences.getInstance(); - // Reload to ensure we get the latest data - await prefs.reload(); - final jsonString = prefs.getString(_projectsKey); + final file = await _projectsFile; - if (jsonString == null || jsonString.isEmpty) { + // Check if file exists + if (!await file.exists()) { return []; } - final List jsonList = json.decode(jsonString); + final contents = await file.readAsString(); + + if (contents.isEmpty) { + return []; + } + + final List jsonList = json.decode(contents); return jsonList.map((json) => Project.fromJson(json)).toList(); } catch (e) { return []; } } - Future saveCurrentPort(int port) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setInt(_currentPortKey, port); + Future getProjectsFilePath() async { + final file = await _projectsFile; + return file.path; } - Future loadCurrentPort() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getInt(_currentPortKey) ?? 8888; + Future importProjectsFromFile(String filePath) async { + try { + final importFile = File(filePath); + + if (!await importFile.exists()) { + return false; + } + + final contents = await importFile.readAsString(); + final List jsonList = json.decode(contents); + final projects = jsonList.map((json) => Project.fromJson(json)).toList(); + + // Save the imported projects + await saveProjects(projects); + return true; + } catch (e) { + return false; + } + } + + Future exportProjectsToFile(String filePath, List projects) async { + try { + final exportFile = File(filePath); + final jsonList = projects.map((p) => p.toJson()).toList(); + final jsonString = const JsonEncoder.withIndent(' ').convert(jsonList); + await exportFile.writeAsString(jsonString); + return true; + } catch (e) { + return false; + } } } - - diff --git a/lib/widgets/about_dialog.dart b/lib/widgets/about_dialog.dart index 8d0d7dc..0142333 100644 --- a/lib/widgets/about_dialog.dart +++ b/lib/widgets/about_dialog.dart @@ -35,7 +35,7 @@ class AppAboutDialog extends StatelessWidget { const Text('• Configure custom UDP ports'), const Text('• Store pre-defined packages'), const Text('• Quick send functionality'), - const Text('• Persistent local storage'), + const Text('• JSON file storage (easily shareable)'), const SizedBox(height: 16), const Divider(), const SizedBox(height: 16), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 724bb2a..b8e2b22 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,10 @@ import FlutterMacOS import Foundation +import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 9cc9e34..e3da014 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -360,6 +360,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" + url: "https://pub.dev" + source: hosted + version: "2.2.19" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.dev" + source: hosted + version: "2.4.2" path_provider_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a5a020b..1f13156 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,9 @@ dependencies: # Local storage for persistence shared_preferences: ^2.2.2 + # File path access + path_provider: ^2.1.2 + # JSON serialization json_annotation: ^4.9.0