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
8 changes: 8 additions & 0 deletions apps/geolibre-desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
"identifier": "fs:allow-remove",
"allow": [{ "path": "$TEMP/geolibre-gdb-*.geojson" }]
},
{
"comment": "Restorable copies of the startup project (GeoLibre#1948). On Android a project picked from device storage is a content:// URI whose read grant dies with the process, so the startup restore has nothing to reopen; GeoLibre keeps its own copy here instead. Only a scope entry is needed, no new command permission: `mkdir` is already granted by `fs:default` (its `create-app-specific-dirs` set), and `read_text_file`/`write_text_file` by the entries above. It must be `fs:scope` rather than a path on those entries because an fs permission's scope applies only to the commands that permission grants, so `fs:default`'s app-directory scope reaches `read_text_file` but not `mkdir` or `write_text_file` (the app's other writes are to paths a dialog just added to the runtime scope) — without this the copies fail with \"forbidden path\". Two entries because the directory itself is the mkdir target and its files are the read/write targets. See lib/startup-project-snapshot.ts.",
"identifier": "fs:scope",
"allow": [
{ "path": "$APPLOCALDATA/startup-projects" },
{ "path": "$APPLOCALDATA/startup-projects/*" }
]
},
Comment thread
giswqs marked this conversation as resolved.
{
"identifier": "opener:allow-open-url",
"allow": [{ "url": "http://*" }, { "url": "https://*" }]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ import { THEME_SCHEMES, normalizeHexColor, type ThemeScheme } from "../../lib/th
import { IS_MAS_BUILD } from "../../lib/build-flags";
import { resolveShareHost, shareHostLabel } from "../../lib/share-geolibre";
import { IS_STORE_BUILD, type UpdateNotificationLevel } from "../../lib/updates";
import { openProjectFile } from "../../lib/tauri-io";
import { ensureStartupProjectSnapshot, openProjectFile } from "../../lib/tauri-io";
import {
DATA_SOURCE_CATALOG,
DATA_SOURCE_SECTION_LABEL_KEYS,
Expand Down Expand Up @@ -1189,6 +1189,13 @@ export function SettingsDialog({
updates: draftDesktopSettings.updates,
startup: draftDesktopSettings.startup,
});
// On Android the project behind the preference just saved is reachable only
// until this process ends, so keep a copy the next launch can open
// (GeoLibre#1948). A no-op on every other platform.
void ensureStartupProjectSnapshot(
draftDesktopSettings.startup,
useAppStore.getState().recentProjects,
);
// The dockable panels are the one layout row nothing renders from the store:
// the registry owns what is on screen, so move it to match what was just
// saved (a no-op for a panel already there, so an untouched Save cannot
Expand Down
41 changes: 41 additions & 0 deletions apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ import {
RecentProjectGoneError,
saveProjectFile,
saveProjectFileToPath,
saveStartupProjectSnapshot,
saveTextFileWithFallback,
} from "../lib/tauri-io";
import { useDesktopSettingsStore } from "./useDesktopSettings";
import { buildProjectHtml } from "../lib/html-export";
import { ensureHtmlFileName, ensureProjectFileName } from "../lib/file-names";
import { mergeStringLists } from "../lib/string-lists";
Expand All @@ -51,6 +53,7 @@ import {
saveChoicesForProject,
type ProjectSaveChoices,
} from "../lib/project-save-choices";
import { startupSettingsAfterForcedSaveAs } from "../lib/startup-project";
import { resolveProjectXyzLayers } from "../lib/xyz-url";
import {
importQgisProject,
Expand Down Expand Up @@ -338,13 +341,27 @@ export function useProjectFileActions(mapControllerRef: MapControllerRef) {
settleSaveNamePrompt,
]);

// On Android a project lives behind a `content://` URI whose read grant dies
// with the process, so the startup restore has nothing to reopen on the next
// launch (GeoLibre#1948). Keep a copy in the app's own storage whenever the
// startup preference points at the project being opened or saved. Fire and
// forget: a failed copy is logged inside and must not fail the open or save.
const rememberStartupProjectSnapshot = (path: string, text: string) => {
void saveStartupProjectSnapshot(
path,
text,
useDesktopSettingsStore.getState().desktopSettings.startup,
);
};

const handleOpenFromFile = async () => {
const result = await openProjectFile();
if (result) {
try {
loadProject(await resolveProjectXyzLayers(result.project), result.path, {
rememberRecent: isTauri(),
});
rememberStartupProjectSnapshot(result.path, result.text);
Comment thread
giswqs marked this conversation as resolved.
} catch (error) {
console.error("Failed to open project", error);
setActionError(
Expand Down Expand Up @@ -685,6 +702,13 @@ export function useProjectFileActions(mapControllerRef: MapControllerRef) {
const project = await resolveProjectXyzLayers(result.project, controller.signal);
if (controller.signal.aborted) return null;
loadProject(project, result.path);
// `loadProject` moves this path to the front of the recent list, so in
// "last" mode it is now the project the next launch will reopen. Without
// this, reopening an older project from the recent list would leave the
// copy on disk holding whichever project was opened through the picker
// last, and the next cold start would find no copy matching the path it
// resolves (GeoLibre#1948 review).
rememberStartupProjectSnapshot(result.path, result.text);
Comment thread
giswqs marked this conversation as resolved.
return null;
} catch (error) {
if (controller.signal.aborted) return null;
Expand Down Expand Up @@ -1107,6 +1131,23 @@ export function useProjectFileActions(mapControllerRef: MapControllerRef) {
name: project.name,
openedAt: new Date().toISOString(),
});
// An ordinary Save that landed somewhere else is Android refusing to write
// the picked document and the save dialog creating a new one in its place
// (GeoLibre#1833). Move a startup preference pinned to the old document
// across, or it keeps naming one nothing can open again. Before the copy
// below, so that copy lands in the slot the moved preference resolves to.
const startupSettings = useDesktopSettingsStore.getState().desktopSettings;
const movedStartup = options?.saveAs
? null
: startupSettingsAfterForcedSaveAs(startupSettings.startup, existingLocalPath, path);
if (movedStartup) {
useDesktopSettingsStore
.getState()
.setDesktopSettings({ ...startupSettings, startup: movedStartup });
}
// Refresh the restorable copy so a startup restore reopens what was just
// saved rather than the state the project was opened in.
rememberStartupProjectSnapshot(path, contentToSave);
markSaved();
recordExplicitProjectSave();
return true;
Expand Down
Loading
Loading