Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion docs/packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
15 changes: 10 additions & 5 deletions docs/theme-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 105 additions & 1 deletion lib/core/storage/app_data_root.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -24,11 +36,15 @@ abstract final class AppDataRoot {
@visibleForTesting
static Map<String, String>? mockEnvironment;

@visibleForTesting
static List<Directory>? mockLegacySupportCandidates;

@visibleForTesting
static void resetMocks() {
mockPortableRootPath = null;
mockInstallDirectory = null;
mockEnvironment = null;
mockLegacySupportCandidates = null;
}

static Map<String, String> get _env =>
Expand Down Expand Up @@ -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<Directory> 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<bool> migrateLegacySupportIfNeeded({
required Directory newSupport,
List<Directory>? 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<List<Directory>> 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<void> _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);
}
}
}
}
2 changes: 1 addition & 1 deletion linux/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions macos/Runner/Configs/AppInfo.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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.
36 changes: 36 additions & 0 deletions test/core/storage/app_data_root_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down Expand Up @@ -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);
});
});
}
8 changes: 4 additions & 4 deletions windows/runner/Runner.rc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading