diff --git a/docs/packaging.md b/docs/packaging.md index 6a4912af..3f041d99 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -38,8 +38,19 @@ Connection passwords remain in the **OS keyring** (`flutter_secure_storage` / libsecret / Credential Manager / Keychain). Portable mode does **not** move secrets into `QueryaData` in v1. +## Bundle / application IDs + +| Platform | ID | +|----------|----| +| Linux (`APPLICATION_ID`) | `com.queryahub.querya_desktop` | +| macOS (bundle id) | `com.queryahub.queryaDesktop` | +| Windows (Company / Product) | `QueryaHub` / `Querya Desktop` | + +Legacy `com.example.*` support directories are migrated once into the new paths +(see `AppDataRoot.migrateLegacySupportIfNeeded`). + ## Related code -- `lib/core/storage/app_data_root.dart` — detection and support-dir redirect +- `lib/core/storage/app_data_root.dart` — portable detection, support-dir redirect, id migration - Updater packaging context: `lib/core/updater/installers/update_install_context.dart` - Release workflow: `.github/workflows/release.yml` diff --git a/docs/theme-import.md b/docs/theme-import.md index cd8c8b99..5043e825 100644 --- a/docs/theme-import.md +++ b/docs/theme-import.md @@ -77,14 +77,19 @@ Querya loads themes from the **application support** directory (see | `assets/themes/` (bundled) | Built-in themes shipped with the app (e.g. Cyberpunk Neon) | `{appSupport}` is normally the OS-specific support folder for Querya Desktop -(`com.example.querya_desktop`). In portable mode it is the `QueryaData/` -sidecar next to the binary — see [packaging.md](packaging.md). Typical OS examples: +(`com.queryahub.querya_desktop` / `com.queryahub.queryaDesktop`). In portable +mode it is the `QueryaData/` sidecar next to the binary — see +[packaging.md](packaging.md). Typical OS examples: | OS | Example path | |----|----------------| -| Linux | `~/.local/share/com.example.querya_desktop/themes` | -| macOS | `~/Library/Application Support/com.example.querya_desktop/themes` | -| Windows | `%APPDATA%\com.example.querya_desktop\themes` | +| Linux | `~/.local/share/com.queryahub.querya_desktop/themes` | +| macOS | `~/Library/Application Support/com.queryahub.queryaDesktop/themes` | +| Windows | `%APPDATA%\QueryaHub\Querya Desktop\themes` | + +On first launch after the bundle-id change, Querya copies an existing profile +from the legacy `com.example.*` support path when the new location has no +`querya.db` yet. **Workflow:** copy or import a theme file into `themes/`. A **file watcher** (0.4.3+) debounces changes under `themes/` and refreshes the registry automatically; use diff --git a/lib/core/storage/app_data_root.dart b/lib/core/storage/app_data_root.dart index a43d5e80..6c34e044 100644 --- a/lib/core/storage/app_data_root.dart +++ b/lib/core/storage/app_data_root.dart @@ -15,6 +15,18 @@ abstract final class AppDataRoot { static const envPortable = 'QUERYA_PORTABLE'; static const sidecarDirName = 'QueryaData'; + /// Current Linux GTK / XDG application id. + static const linuxApplicationId = 'com.queryahub.querya_desktop'; + + /// Current macOS bundle identifier. + static const macBundleId = 'com.queryahub.queryaDesktop'; + + /// Previous placeholder ids (pre-#385) used for one-shot support-dir migration. + static const legacyLinuxApplicationId = 'com.example.querya_desktop'; + static const legacyMacBundleId = 'com.example.queryaDesktop'; + static const legacyWindowsCompany = 'com.example'; + static const legacyWindowsProduct = 'querya_desktop'; + @visibleForTesting static String? mockPortableRootPath; @@ -24,11 +36,15 @@ abstract final class AppDataRoot { @visibleForTesting static Map? mockEnvironment; + @visibleForTesting + static List? mockLegacySupportCandidates; + @visibleForTesting static void resetMocks() { mockPortableRootPath = null; mockInstallDirectory = null; mockEnvironment = null; + mockLegacySupportCandidates = null; } static Map get _env => @@ -85,9 +101,97 @@ abstract final class AppDataRoot { (await resolvePortableRoot()) != null; /// Application-support equivalent: portable root or [getApplicationSupportDirectory]. + /// + /// When not portable, copies data once from legacy `com.example.*` support + /// paths if the new location has no `querya.db` yet. static Future applicationSupportDirectory() async { final portable = await resolvePortableRoot(); if (portable != null) return portable; - return getApplicationSupportDirectory(); + + final support = await getApplicationSupportDirectory(); + await migrateLegacySupportIfNeeded(newSupport: support); + return support; + } + + /// One-shot copy from [legacySupportCandidates] into [newSupport]. + @visibleForTesting + static Future migrateLegacySupportIfNeeded({ + required Directory newSupport, + List? legacyCandidates, + }) async { + final newDb = File(p.join(newSupport.path, 'querya_desktop', 'querya.db')); + if (await newDb.exists()) return false; + + final candidates = legacyCandidates ?? + mockLegacySupportCandidates ?? + await legacySupportCandidates(); + + for (final legacy in candidates) { + if (p.equals(legacy.path, newSupport.path)) continue; + final legacyDb = + File(p.join(legacy.path, 'querya_desktop', 'querya.db')); + if (!await legacyDb.exists()) continue; + await _copyDirectory(legacy, newSupport); + debugPrint( + 'AppDataRoot: migrated profile data from ${legacy.path} → ${newSupport.path}', + ); + return true; + } + return false; + } + + @visibleForTesting + static Future> legacySupportCandidates() async { + final home = _env['HOME'] ?? _env['USERPROFILE']; + if (home == null || home.isEmpty) return const []; + + if (Platform.isLinux) { + final xdg = _env['XDG_DATA_HOME']; + final base = + (xdg != null && xdg.isNotEmpty) ? xdg : p.join(home, '.local', 'share'); + return [Directory(p.join(base, legacyLinuxApplicationId))]; + } + if (Platform.isMacOS) { + return [ + Directory( + p.join(home, 'Library', 'Application Support', legacyMacBundleId), + ), + // Older docs incorrectly used the Linux-style id on macOS. + Directory( + p.join( + home, + 'Library', + 'Application Support', + legacyLinuxApplicationId, + ), + ), + ]; + } + if (Platform.isWindows) { + final appData = + _env['APPDATA'] ?? p.join(home, 'AppData', 'Roaming'); + return [ + Directory( + p.join(appData, legacyWindowsCompany, legacyWindowsProduct), + ), + ]; + } + return const []; + } + + static Future _copyDirectory(Directory from, Directory to) async { + if (!await to.exists()) { + await to.create(recursive: true); + } + await for (final entity in from.list(recursive: true, followLinks: false)) { + final relative = p.relative(entity.path, from: from.path); + final destPath = p.join(to.path, relative); + if (entity is Directory) { + await Directory(destPath).create(recursive: true); + } else if (entity is File) { + await File(destPath).parent.create(recursive: true); + await entity.copy(destPath); + } + } } } diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index fab71312..2c70013b 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -7,7 +7,7 @@ project(runner LANGUAGES CXX) set(BINARY_NAME "querya_desktop") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.querya_desktop") +set(APPLICATION_ID "com.queryahub.querya_desktop") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 37d3fbdf..8a6a46eb 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = querya_desktop // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.queryaDesktop +PRODUCT_BUNDLE_IDENTIFIER = com.queryahub.queryaDesktop // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2026 QueryaHub. All rights reserved. diff --git a/test/core/storage/app_data_root_test.dart b/test/core/storage/app_data_root_test.dart index 8b2ec252..6a9dc1ed 100644 --- a/test/core/storage/app_data_root_test.dart +++ b/test/core/storage/app_data_root_test.dart @@ -33,6 +33,8 @@ void main() { PathProviderPlatform.instance = _FakePathProvider(osSupport.path); AppDataRoot.resetMocks(); AppDataRoot.mockInstallDirectory = installDir.path; + // Avoid touching the developer's real ~/.local/share/com.example.* tree. + AppDataRoot.mockLegacySupportCandidates = const []; ExtensionPaths.mockExtensionsDirectory = null; SandboxLogPaths.mockLogsDirectory = null; }); @@ -101,5 +103,39 @@ void main() { final logs = await SandboxLogPaths.logsDirectory(); expect(logs.path, p.join(portable.path, 'logs')); }); + + test('migrates legacy support dir when new location has no querya.db', + () async { + final legacy = Directory(p.join(tempDir.path, 'legacy_support')); + final next = Directory(p.join(tempDir.path, 'new_support')); + await Directory(p.join(legacy.path, 'querya_desktop')) + .create(recursive: true); + await File(p.join(legacy.path, 'querya_desktop', 'querya.db')) + .writeAsString('legacy-db'); + await Directory(p.join(legacy.path, 'themes')).create(recursive: true); + await File(p.join(legacy.path, 'themes', 'a.json')).writeAsString('{}'); + + final migrated = await AppDataRoot.migrateLegacySupportIfNeeded( + newSupport: next, + legacyCandidates: [legacy], + ); + + expect(migrated, isTrue); + expect( + await File(p.join(next.path, 'querya_desktop', 'querya.db')) + .readAsString(), + 'legacy-db', + ); + expect( + await File(p.join(next.path, 'themes', 'a.json')).exists(), + isTrue, + ); + + final again = await AppDataRoot.migrateLegacySupportIfNeeded( + newSupport: next, + legacyCandidates: [legacy], + ); + expect(again, isFalse); + }); }); } diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 0b8972a7..45d4c679 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -89,13 +89,13 @@ BEGIN BEGIN BLOCK "040904e4" BEGIN - VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "querya_desktop" "\0" + VALUE "CompanyName", "QueryaHub" "\0" + VALUE "FileDescription", "Querya Desktop" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "querya_desktop" "\0" - VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 QueryaHub. All rights reserved." "\0" VALUE "OriginalFilename", "querya_desktop.exe" "\0" - VALUE "ProductName", "querya_desktop" "\0" + VALUE "ProductName", "Querya Desktop" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END