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
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ jobs:
run: |
$v = "${{ steps.version.outputs.version }}"
$dir = "build\windows\x64\runner\Release"
New-Item -ItemType File -Path "$dir\.portable" -Force | Out-Null
New-Item -ItemType File -Path "$dir\portable.txt" -Force | Out-Null
Compress-Archive -Path "$dir\*" -DestinationPath "Querya-Desktop-$v-windows.zip"
Remove-Item -Path "$dir\.portable", "$dir\portable.txt" -Force

- name: Install Inno Setup
run: choco install innosetup --no-progress -y
Expand Down Expand Up @@ -143,7 +146,9 @@ jobs:
- name: Zip Linux artifact (portable)
run: |
cd build/linux/x64/release/bundle
touch .portable portable.txt
zip -r "${GITHUB_WORKSPACE}/Querya-Desktop-${{ steps.version.outputs.version }}-linux.zip" .
rm -f .portable portable.txt

- name: Build Linux AppImage (installable)
run: ./scripts/linux/build_appimage.sh
Expand Down
19 changes: 9 additions & 10 deletions docs/packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,20 @@ Querya Desktop ships (and will ship) two download channels. See epic

| Channel | Typical artifact | Profile data |
|---------|------------------|--------------|
| **Portable** | `Querya-Desktop-{ver}-{os}.zip` (Flutter bundle) | OS app-support by default; optional sidecar — see below |
| **Portable** | `Querya-Desktop-{ver}-{os}.zip` (Flutter bundle) | Portable sidecar (`QueryaData/`) automatically via `.portable` marker |
| **Installable** | AppImage, Windows setup, `.deb`, `.rpm`, Flatpak | Normal OS locations |

## Portable profile (`QueryaData`)

By default the zip is a **relocatable binary** only: settings DB, themes, and
extensions still use OS paths (`getApplicationSupportDirectory`,
`~/.querya/extensions`, …).
Official release zip archives include a `.portable` marker file out-of-the-box.
On launch, Querya detects the marker (or explicit `QueryaData` / `QUERYA_PORTABLE=1`)
and automatically creates `QueryaData/` next to the binary if it does not exist yet.

To keep profile data next to the app (USB-style):

1. Set environment variable **`QUERYA_PORTABLE=1`** (also `true` / `yes` / `on`),
**or**
2. Create a folder named **`QueryaData`** next to `querya_desktop` /
`querya_desktop.exe` / the `.AppImage` file.
To run in portable mode:
1. Use the pre-packaged portable zip (contains `.portable` marker file out-of-the-box),
2. **or** create a `.portable` or `portable.txt` file next to the binary,
3. **or** create a folder named **`QueryaData`** next to the binary,
4. **or** set environment variable **`QUERYA_PORTABLE=1`** (also `true` / `yes` / `on`).

Then local data is stored under that folder:

Expand Down
7 changes: 6 additions & 1 deletion lib/core/storage/app_data_root.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import 'package:path_provider/path_provider.dart';
abstract final class AppDataRoot {
static const envPortable = 'QUERYA_PORTABLE';
static const sidecarDirName = 'QueryaData';
static const portableMarkerFile = '.portable';
static const portableMarkerAlt = 'portable.txt';

/// Current Linux GTK / XDG application id.
static const linuxApplicationId = 'com.queryahub.querya_desktop';
Expand Down Expand Up @@ -83,8 +85,11 @@ abstract final class AppDataRoot {

final sidecar = Directory(p.join(installDir, sidecarDirName));
final forced = envRequestsPortable();
final hasMarker =
await File(p.join(installDir, portableMarkerFile)).exists() ||
await File(p.join(installDir, portableMarkerAlt)).exists();

if (forced) {
if (forced || hasMarker) {
if (!await sidecar.exists()) {
await sidecar.create(recursive: true);
}
Expand Down
44 changes: 44 additions & 0 deletions lib/features/settings/preferences_about_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,50 @@ class PreferencesAboutSection extends StatelessWidget {
).muted().xSmall(),
const material.SizedBox(height: 12),

FutureBuilder<bool>(
future: AppDataRoot.isPortableMode(),
builder: (context, snapshot) {
final isPortable = snapshot.data ?? false;
if (!isPortable) return const SizedBox.shrink();
return material.Container(
margin: const material.EdgeInsets.only(bottom: 12),
padding: const material.EdgeInsets.all(12),
decoration: material.BoxDecoration(
color: theme.colorScheme.muted.withValues(alpha: 0.25),
borderRadius: material.BorderRadius.circular(6),
border: material.Border.all(
color: theme.colorScheme.primary.withValues(alpha: 0.3),
),
),
child: material.Row(
crossAxisAlignment: material.CrossAxisAlignment.start,
children: [
const QueryaBadge.status(
'PORTABLE',
status: QueryaBadgeStatus.info,
),
const material.SizedBox(width: 10),
material.Expanded(
child: material.Column(
crossAxisAlignment: material.CrossAxisAlignment.start,
children: [
const Text('Portable Mode Active')
.semiBold()
.small()
.foreground(),
const material.SizedBox(height: 2),
const Text(
'Data, settings, and themes are stored in the local QueryaData folder. Connection passwords remain protected in the host OS keyring.',
).muted().xSmall(),
],
),
),
],
),
);
},
),

material.Wrap(
spacing: 8,
runSpacing: 8,
Expand Down
22 changes: 22 additions & 0 deletions test/core/storage/app_data_root_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,28 @@ void main() {
expect(await AppDataRoot.isPortableMode(), isTrue);
});

test('.portable marker file automatically enables portable mode and creates QueryaData', () async {
final marker = File(p.join(installDir.path, '.portable'));
await marker.writeAsString('');

final portable = await AppDataRoot.resolvePortableRoot();
expect(portable, isNotNull);
expect(portable!.path, p.join(installDir.path, 'QueryaData'));
expect(await portable.exists(), isTrue);
expect(await AppDataRoot.isPortableMode(), isTrue);
});

test('portable.txt marker file enables portable mode', () async {
final marker = File(p.join(installDir.path, 'portable.txt'));
await marker.writeAsString('');

final portable = await AppDataRoot.resolvePortableRoot();
expect(portable, isNotNull);
expect(portable!.path, p.join(installDir.path, 'QueryaData'));
expect(await portable.exists(), isTrue);
expect(await AppDataRoot.isPortableMode(), isTrue);
});

test('APPIMAGE parent is used as install directory', () async {
final appImage = p.join(installDir.path, 'Querya.AppImage');
AppDataRoot.mockInstallDirectory = null;
Expand Down
Loading