From 70eed3339500d0dcb74c7d22c2e16cd3471024de Mon Sep 17 00:00:00 2001 From: ProjectEdge-Jim Date: Fri, 30 Jan 2026 19:41:34 -0500 Subject: [PATCH 1/2] fix: use local app data directory on desktop platforms On desktop platforms, the Documents folder is often synced to cloud services (OneDrive, iCloud, Dropbox). Large model files (~3.6GB) in these folders cause issues with native code file access - specifically, LiteRT-LM's JNI layer cannot reliably access files in cloud-synced locations. This change uses platform-specific local app data directories that are never synced: - Windows: %LOCALAPPDATA%\flutter_gemma (truly local, never synced) - macOS: ~/Library/Application Support/flutter_gemma - Linux: ~/.local/share/flutter_gemma (respects XDG_DATA_HOME) Mobile platforms (Android/iOS) continue to use the Documents directory as before. Fixes issues with OneDrive/iCloud/Dropbox sync interfering with model loading on desktop platforms. --- .../platform_file_system_service.dart | 48 ++++++++++++- .../utils/file_system_manager.dart | 68 ++++++++++++++++++- 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/lib/core/infrastructure/platform_file_system_service.dart b/lib/core/infrastructure/platform_file_system_service.dart index 1c524f72..7d005fd1 100644 --- a/lib/core/infrastructure/platform_file_system_service.dart +++ b/lib/core/infrastructure/platform_file_system_service.dart @@ -132,7 +132,18 @@ class PlatformFileSystemService implements FileSystemService { // The actual tracking is done in ProtectedFilesRegistry.registerExternalPath() } - /// Gets the app documents directory with caching + /// Gets the model storage directory with caching. + /// + /// On desktop platforms, uses platform-specific local app data directories + /// that are NOT synced to cloud services (OneDrive, iCloud, Dropbox). + /// This is critical because native code (LiteRT-LM JNI) cannot reliably + /// access files in cloud-synced folders. + /// + /// Storage locations: + /// - Windows: %LOCALAPPDATA%\flutter_gemma (truly local, never synced) + /// - macOS: ~/Library/Application Support/flutter_gemma + /// - Linux: ~/.local/share/flutter_gemma + /// - Android/iOS: App documents directory (standard behavior) Future _getDocumentsDirectory() async { // Web doesn't support local file system if (kIsWeb) { @@ -143,7 +154,40 @@ class PlatformFileSystemService implements FileSystemService { return _documentsDirectory!; } - _documentsDirectory = await getApplicationDocumentsDirectory(); + if (Platform.isWindows) { + // Use LOCALAPPDATA on Windows - truly local and never synced + final localAppData = Platform.environment['LOCALAPPDATA']; + if (localAppData != null) { + _documentsDirectory = Directory(path.join(localAppData, 'OroForge', 'models')); + } else { + _documentsDirectory = await getApplicationSupportDirectory(); + } + } else if (Platform.isMacOS) { + // Use Application Support on macOS - not synced by default + final home = Platform.environment['HOME']; + if (home != null) { + _documentsDirectory = Directory(path.join(home, 'Library', 'Application Support', 'OroForge', 'models')); + } else { + _documentsDirectory = await getApplicationSupportDirectory(); + } + } else if (Platform.isLinux) { + // Use XDG data directory on Linux + final xdgDataHome = Platform.environment['XDG_DATA_HOME']; + if (xdgDataHome != null) { + _documentsDirectory = Directory(path.join(xdgDataHome, 'OroForge', 'models')); + } else { + final home = Platform.environment['HOME']; + if (home != null) { + _documentsDirectory = Directory(path.join(home, '.local', 'share', 'OroForge', 'models')); + } else { + _documentsDirectory = await getApplicationSupportDirectory(); + } + } + } else { + // Mobile platforms use standard documents directory + _documentsDirectory = await getApplicationDocumentsDirectory(); + } + return _documentsDirectory!; } } diff --git a/lib/core/model_management/utils/file_system_manager.dart b/lib/core/model_management/utils/file_system_manager.dart index 70190bd4..0c0ff089 100644 --- a/lib/core/model_management/utils/file_system_manager.dart +++ b/lib/core/model_management/utils/file_system_manager.dart @@ -76,9 +76,71 @@ class ModelFileSystemManager { } } + /// Cached model storage directory + static Directory? _modelStorageDirectory; + + /// Gets the model storage directory. + /// + /// On desktop platforms, uses local app data directories that are NOT synced + /// to cloud services (OneDrive, iCloud, Dropbox). This is critical because + /// native code (LiteRT-LM JNI) cannot reliably access files in cloud-synced + /// folders. + /// + /// Storage locations: + /// - Windows: %LOCALAPPDATA%\flutter_gemma (truly local, never synced) + /// - macOS: ~/Library/Application Support/flutter_gemma + /// - Linux: ~/.local/share/flutter_gemma + /// - Android/iOS: App documents directory (standard behavior) + static Future getModelStorageDirectory() async { + if (_modelStorageDirectory != null) { + return _modelStorageDirectory!; + } + + if (Platform.isWindows) { + // Use LOCALAPPDATA on Windows - truly local and never synced + final localAppData = Platform.environment['LOCALAPPDATA']; + if (localAppData != null) { + _modelStorageDirectory = Directory(path.join(localAppData, 'OroForge', 'models')); + } else { + _modelStorageDirectory = await getApplicationSupportDirectory(); + } + } else if (Platform.isMacOS) { + // Use Application Support on macOS - not synced by default + final localAppData = Platform.environment['HOME']; + if (localAppData != null) { + _modelStorageDirectory = Directory(path.join(localAppData, 'Library', 'Application Support', 'OroForge', 'models')); + } else { + _modelStorageDirectory = await getApplicationSupportDirectory(); + } + } else if (Platform.isLinux) { + // Use XDG data directory on Linux + final xdgDataHome = Platform.environment['XDG_DATA_HOME']; + if (xdgDataHome != null) { + _modelStorageDirectory = Directory(path.join(xdgDataHome, 'OroForge', 'models')); + } else { + final home = Platform.environment['HOME']; + if (home != null) { + _modelStorageDirectory = Directory(path.join(home, '.local', 'share', 'OroForge', 'models')); + } else { + _modelStorageDirectory = await getApplicationSupportDirectory(); + } + } + } else { + // Mobile platforms use standard documents directory + _modelStorageDirectory = await getApplicationDocumentsDirectory(); + } + + // Ensure directory exists + if (!await _modelStorageDirectory!.exists()) { + await _modelStorageDirectory!.create(recursive: true); + } + + return _modelStorageDirectory!; + } + /// Gets the full file path for a model file with Android path correction static Future getModelFilePath(String filename) async { - final directory = await getApplicationDocumentsDirectory(); + final directory = await getModelStorageDirectory(); return getCorrectedPath(directory.path, filename); } @@ -90,7 +152,7 @@ class ModelFileSystemManager { List? protectedFiles, List? supportedExtensions, }) async { - final directory = await getApplicationDocumentsDirectory(); + final directory = await getModelStorageDirectory(); final extensions = supportedExtensions ?? ModelFileSystemManager.supportedExtensions; final protected = protectedFiles ?? []; @@ -132,7 +194,7 @@ class ModelFileSystemManager { static Future getStorageInfo({ List? protectedFiles, }) async { - final directory = await getApplicationDocumentsDirectory(); + final directory = await getModelStorageDirectory(); final files = directory .listSync() From 8ce910046bc40ed6566fd1fe758451a22340b014 Mon Sep 17 00:00:00 2001 From: ProjectEdge-Jim Date: Fri, 30 Jan 2026 20:53:57 -0500 Subject: [PATCH 2/2] Fix Windows path handling in SmartDownloader Task.split() has a bug where it strips the drive letter from Windows paths that don't match known base directories. This fix manually handles Windows paths to preserve the full directory path including drive letter. Fixes model downloads to custom directories like %LOCALAPPDATA%\OroForge\models --- lib/mobile/smart_downloader.dart | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/mobile/smart_downloader.dart b/lib/mobile/smart_downloader.dart index b2ec90a2..eb68c100 100644 --- a/lib/mobile/smart_downloader.dart +++ b/lib/mobile/smart_downloader.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'dart:io' show Platform; import 'package:flutter/foundation.dart'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter_gemma/core/domain/download_error.dart'; import 'package:flutter_gemma/core/domain/download_exception.dart'; import 'package:flutter_gemma/core/model_management/cancel_token.dart'; +import 'package:path/path.dart' as p; /// Smart downloader with HTTP-aware retry logic /// @@ -177,7 +179,28 @@ class SmartDownloader { StreamSubscription? listener; try { - final (baseDirectory, directory, filename) = await Task.split(filePath: targetPath); + // CRITICAL: Task.split() has a bug on Windows where it strips the drive letter + // from paths that don't match known base directories, causing files to be written + // to the wrong location. We handle Windows paths manually to preserve the full path. + final BaseDirectory baseDirectory; + final String directory; + final String filename; + + if (!kIsWeb && Platform.isWindows) { + // On Windows, use BaseDirectory.root with the FULL directory path including drive letter. + // background_downloader's baseDirectoryPath(BaseDirectory.root) returns '' on Windows, + // so path.join('', fullDirectory, filename) correctly produces the absolute path. + baseDirectory = BaseDirectory.root; + directory = p.dirname(targetPath); // Full directory with drive letter (e.g., C:\Users\...) + filename = p.basename(targetPath); + debugPrint('🔵 Windows path handling: dir=$directory, file=$filename'); + } else { + // On other platforms, Task.split() works correctly + final splitResult = await Task.split(filePath: targetPath); + baseDirectory = splitResult.$1; + directory = splitResult.$2; + filename = splitResult.$3; + } final task = DownloadTask( url: url,