Badge the exception (not yet cached) instead of the near-universal case
GpxStore#save converts eagerly, so "cached" is true for nearly every file in the tree already — badging that was just noise. Flipped to PENDING_ICON, shown only for the rare case: a .gpx/.fit dropped straight into data/gpx/ on the filesystem, bypassing the app, and not yet opened. Also add an explicit download button next to delete (DOWNLOAD_ICON) — the file name itself has been a download link since the last commit, but isn't obviously clickable at a glance.
This commit is contained in:
1 parent
cd92dadf6a
commit
12635a48c3
4 files changed
+73
-26
No files matched your search
+19
-14
@@ -282,26 +282,31 @@ multi-second hang on a checkbox click reads as broken in a way the
|
|||||||
same wait during an upload doesn't. A file that fails to parse still
|
same wait during an upload doesn't. A file that fails to parse still
|
||||||
saves fine either way; the error just surfaces on first view instead.
|
saves fine either way; the error just surfaces on first view instead.
|
||||||
A `.geojson` upload skips this path entirely — `geojson` reads and
|
A `.geojson` upload skips this path entirely — `geojson` reads and
|
||||||
returns it directly, no cache file, no `Gpx`/`Fit` involved; the file
|
returns it directly, no cache file, no `Gpx`/`Fit` involved.
|
||||||
tree's cached-file icon (`GEOJSON_ICON`, `public/js/icons.js`) always
|
`GpxStore#save` converting eagerly means `cached` is `true` for
|
||||||
shows for one, since there's no conversion step it could ever be
|
almost everything in the tree — the one exception is a `.gpx`/`.fit`
|
||||||
waiting on.
|
dropped straight into `data/gpx/` on the filesystem, bypassing the
|
||||||
|
app, and not yet opened. Badging the common case would just be noise,
|
||||||
|
so the file tree's icon (`PENDING_ICON`, `public/js/icons.js`) marks
|
||||||
|
that exception instead of the (near-universal) ready state.
|
||||||
+
|
+
|
||||||
Once a `.gpx`/`.fit` is cached, the tree shows it under its converted
|
Once a `.gpx`/`.fit` is cached, the tree shows it under its converted
|
||||||
name instead — `"ride.gpx"` reads as `"ride.gpx.geojson"`, matching
|
name instead — `"ride.gpx"` reads as `"ride.gpx.geojson"`, matching
|
||||||
the real cache file sitting next to it on disk — since the GeoJSON,
|
the real cache file sitting next to it on disk — since the GeoJSON,
|
||||||
not the original, is what's actually rendered on the map from that
|
not the original, is what's actually rendered on the map from that
|
||||||
point on. The original is never deleted or re-parsed once cached, just
|
point on. The original is never deleted or re-parsed once cached, just
|
||||||
not re-shown as itself; the tree's file name is a real link
|
not re-shown as itself. Two ways to get that converted form out of the
|
||||||
(`GET /api/gpx/file/<path>?download=1`), the only way to get that
|
app as an actual file (`GET /api/gpx/file/<path>?download=1`) — the
|
||||||
converted form out of the app as an actual file, since the cache is
|
cache is otherwise an implementation detail with no name of its own to
|
||||||
otherwise an implementation detail with no name of its own to expose.
|
expose: the file name itself is a real link (an `<a>`, not a `<span>`,
|
||||||
It's an `<a>`, not a `<span>`, specifically so clicking it downloads
|
specifically so clicking it downloads instead of also toggling the
|
||||||
instead of also toggling the row's checkbox — a native `<a>`/`<button>`
|
row's checkbox — a native `<a>`/`<button>` nested inside a `<label>`
|
||||||
nested inside a `<label>` claims its own click instead of the label
|
claims its own click instead of the label forwarding it to the
|
||||||
forwarding it to the associated control, so the two behaviors don't
|
associated control, so the two behaviors don't fight over the same
|
||||||
fight over the same click. A directly-uploaded `.geojson` keeps its
|
click), and a dedicated download button sits next to delete for anyone
|
||||||
own name (nothing to convert) and downloads unchanged. `Gpx` parses the GPX with REXML: `<wpt>`
|
who wouldn't guess the name itself is clickable. A directly-uploaded
|
||||||
|
`.geojson` keeps its own name (nothing to convert) and downloads
|
||||||
|
unchanged either way. `Gpx` parses the GPX with REXML: `<wpt>`
|
||||||
waypoints become `Point` features (with `name`, `desc` and `time`
|
waypoints become `Point` features (with `name`, `desc` and `time`
|
||||||
carried through as properties), each `<trk>` becomes one
|
carried through as properties), each `<trk>` becomes one
|
||||||
`MultiLineString` feature (one line per `<trkseg>`), and each `<rte>`
|
`MultiLineString` feature (one line per `<trkseg>`), and each `<rte>`
|
||||||
|
|||||||
@@ -379,6 +379,22 @@ html, body {
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gpx-tree__download {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
color: #999;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpx-tree__download svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpx-tree__download:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
.gpx-tree__delete {
|
.gpx-tree__delete {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
+19
-5
@@ -6,7 +6,8 @@ import {
|
|||||||
WAYPOINT_ICON,
|
WAYPOINT_ICON,
|
||||||
TRACK_ICON,
|
TRACK_ICON,
|
||||||
ROUTE_ICON,
|
ROUTE_ICON,
|
||||||
GEOJSON_ICON,
|
PENDING_ICON,
|
||||||
|
DOWNLOAD_ICON,
|
||||||
MAP_MARKER_ICON,
|
MAP_MARKER_ICON,
|
||||||
POI_ICONS,
|
POI_ICONS,
|
||||||
PARK4NIGHT_TAGS,
|
PARK4NIGHT_TAGS,
|
||||||
@@ -290,9 +291,10 @@ export class GpxPanelControl {
|
|||||||
name.title = `Download ${displayName}`;
|
name.title = `Download ${displayName}`;
|
||||||
label.appendChild(checkbox);
|
label.appendChild(checkbox);
|
||||||
(file.kinds || []).forEach((kind) => label.appendChild(this._kindIcon(kind)));
|
(file.kinds || []).forEach((kind) => label.appendChild(this._kindIcon(kind)));
|
||||||
if (file.cached) label.appendChild(this._cachedIcon());
|
if (!file.cached) label.appendChild(this._pendingIcon());
|
||||||
label.appendChild(name);
|
label.appendChild(name);
|
||||||
row.appendChild(label);
|
row.appendChild(label);
|
||||||
|
row.appendChild(this._downloadButton(file));
|
||||||
row.appendChild(this._deleteButton(() => this._deleteFile(file.path)));
|
row.appendChild(this._deleteButton(() => this._deleteFile(file.path)));
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
@@ -314,14 +316,26 @@ export class GpxPanelControl {
|
|||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cachedIcon() {
|
_pendingIcon() {
|
||||||
const icon = document.createElement("span");
|
const icon = document.createElement("span");
|
||||||
icon.className = "gpx-tree__kind-icon";
|
icon.className = "gpx-tree__kind-icon";
|
||||||
icon.innerHTML = GEOJSON_ICON;
|
icon.innerHTML = PENDING_ICON;
|
||||||
icon.title = "Ready to view instantly (GeoJSON cached)";
|
icon.title = "Not yet converted — dropped straight into data/gpx/, not uploaded through the app";
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same destination as clicking the file's name (gpxFileUrl + ?download=1)
|
||||||
|
// — a second, more obviously clickable entry point for the same action.
|
||||||
|
_downloadButton(file) {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.className = "gpx-tree__download";
|
||||||
|
link.href = `${gpxFileUrl(file.path)}?download=1`;
|
||||||
|
link.title = "Download GeoJSON";
|
||||||
|
link.setAttribute("aria-label", "Download GeoJSON");
|
||||||
|
link.innerHTML = DOWNLOAD_ICON;
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
_deleteButton(onClick) {
|
_deleteButton(onClick) {
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
|
|||||||
+19
-7
@@ -54,14 +54,26 @@ export const WAYPOINT_ICON = `
|
|||||||
|
|
||||||
export const TRACK_ICON = GPX_ICON;
|
export const TRACK_ICON = GPX_ICON;
|
||||||
|
|
||||||
// Shown next to a file whose GeoJSON conversion is already cached on disk
|
// Shown next to a file whose GeoJSON conversion is NOT yet cached on disk
|
||||||
// (GpxStore#geojson) — ready to view instantly instead of paying REXML's
|
// (GpxStore#geojson) — the uncommon case, since GpxStore#save converts
|
||||||
// parse cost on click. Curly braces read as "structured data ready", the
|
// eagerly at upload time; this only happens for a .gpx/.fit dropped
|
||||||
// same shorthand most editors/tools use for JSON.
|
// straight into data/gpx/ on the filesystem, bypassing the app, and not
|
||||||
export const GEOJSON_ICON = `
|
// yet opened. Everything uploaded normally is cached almost immediately,
|
||||||
|
// so badging the common case would just be noise — this badges the
|
||||||
|
// exception instead.
|
||||||
|
export const PENDING_ICON = `
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M9 4c-1.5 0-2 .8-2 2v3c0 1-.5 1.5-1.5 1.5 1 0 1.5.5 1.5 1.5v3c0 1.2.5 2 2 2"/>
|
<circle cx="12" cy="12" r="8"/>
|
||||||
<path d="M15 4c1.5 0 2 .8 2 2v3c0 1 .5 1.5 1.5 1.5-1 0-1.5.5-1.5 1.5v3c0 1.2-.5 2-2 2"/>
|
<path d="M12 8v4l3 2"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
// A small download button next to each tree file's delete button — the
|
||||||
|
// name link (see gpx.js's _renderFile) does the same thing, but isn't
|
||||||
|
// obviously clickable at a glance, so this is the discoverable version.
|
||||||
|
export const DOWNLOAD_ICON = `
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 4v11M8 11l4 4 4-4"/>
|
||||||
|
<path d="M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3"/>
|
||||||
</svg>`;
|
</svg>`;
|
||||||
|
|
||||||
// Same shape as TRACK_ICON, dashed — a route is planned rather than
|
// Same shape as TRACK_ICON, dashed — a route is planned rather than
|
||||||
|
|||||||
Reference in new issue
Block a user