Skip to content
Closed
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
3 changes: 3 additions & 0 deletions docs/configuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
**`title`** | `string` | Required | The text to display/ title of a given item. Max length `18`
**`description`** | `string` | _Optional_ | Additional info about an item, which is shown in the tooltip on hover, or visible on large tiles
**`url`** | `string` | _Optional_ | The URL / location of web address for when the item is clicked
**`localUrl`** | `string` | _Optional_ | An alternative URL (e.g. a LAN address) that is preferred whenever it is reachable from your browser. On page load Dashy probes this URL in the background; if it responds, clicking the item opens `localUrl`, otherwise it falls back to `url`. Ideal for local-vs-remote access (Tailscale, VPN, reverse-proxy, etc). The probe runs in the background so it never delays a click
**`localUrlTimeout`** | `number` | _Optional_ | Milliseconds to wait for the `localUrl` reachability probe before giving up and using `url`. Clamped between `300` and `5000`. Defaults to `1500`
**`localUrlCheckInterval`** | `number` | _Optional_ | Seconds between background re-checks of `localUrl`. `0` means only check on page load and when the browser tab regains focus. Clamped to `300` max. Defaults to `0`
**`icon`** | `string` | _Optional_ | The icon for a given item. Can be a font-awesome icon, favicon, remote URL or local URL. See [`item.icon`](#sectionicon-and-sectionitemicon)
**`target`** | `string` | _Optional_ | The opening method for when the item is clicked, either `newtab`, `sametab`, `modal`, `workspace`, `clipboard`, `top` or `parent`. Where `newtab` will open the link in a new tab, `sametab` will open it in the current tab, and `modal` will open a pop-up modal, `workspace` will open in the Workspace view and `clipboard` will copy the URL to system clipboard (but not launch app). Defaults to `newtab`
**`hotkey`** | `number` | _Optional_ | Give frequently opened applications a numeric hotkey, between `0 - 9`. You can then just press that key to launch that application.
Expand Down
8 changes: 7 additions & 1 deletion src/components/LinkItems/Item.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
@contextmenu.prevent
@mouseup.right="openContextMenu"
v-longPress="true"
:href="item.url"
:href="effectiveUrl"
:target="anchorTarget"
:class="`item ${makeClassList}`"
v-tooltip="getTooltipOptions()"
Expand Down Expand Up @@ -218,11 +218,17 @@ export default {
this.intervalId = setInterval(this.checkWebsiteStatus, this.statusCheckInterval * 1000);
}
}
// If an alternative local URL is set, probe its reachability in the background
if (this.hasLocalUrl) {
this.startLocalUrlChecks();
}
},
beforeUnmount() {
// Stop periodic ping-check and status-check when item is destroyed (e.g. navigating in multi-page setup)
if (this.pingIntervalId) clearInterval(this.pingIntervalId);
if (this.intervalId) clearInterval(this.intervalId);
// Stop local-URL probing timers and listeners
this.stopLocalUrlChecks();
},
};
</script>
Expand Down
9 changes: 9 additions & 0 deletions src/components/LinkItems/SubItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ export default {
return {};
},
methods: {},
mounted() {
// If an alternative local URL is set, probe its reachability in the background
if (this.hasLocalUrl) {
this.startLocalUrlChecks();
}
},
beforeUnmount() {
this.stopLocalUrlChecks();
},
};
</script>

Expand Down
73 changes: 70 additions & 3 deletions src/mixins/ItemMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export default {
contextMenuOpen: false,
intervalId: undefined, // status-check setInterval() id
pingIntervalId: undefined, // ping-check setInterval() id
// Local URL reachability: undefined = not yet probed, true/false = last result
localUrlReachable: undefined,
localUrlIntervalId: undefined, // local-url re-check setInterval() id
contextPos: {
posX: undefined,
posY: undefined,
Expand Down Expand Up @@ -107,6 +110,32 @@ export default {
accumulatedTarget() {
return this.item.target || this.appConfig.defaultOpeningMethod || defaultOpeningMethod;
},
/* True if a non-empty alternative local URL has been configured for this item */
hasLocalUrl() {
return !!(this.item.localUrl && typeof this.item.localUrl === 'string'
&& this.item.localUrl.trim());
},
/* Timeout (ms) for the local URL reachability probe, clamped to a sane range */
localUrlProbeTimeout() {
let timeout = this.item.localUrlTimeout;
if (typeof timeout !== 'number' || Number.isNaN(timeout)) timeout = 1500;
if (timeout < 300) timeout = 300;
if (timeout > 5000) timeout = 5000;
return timeout;
},
/* Interval (seconds) between background re-checks; 0 = only on load + tab focus */
localUrlCheckInterval() {
let interval = this.item.localUrlCheckInterval;
if (typeof interval !== 'number' || Number.isNaN(interval) || interval < 0) return 0;
if (interval > 300) interval = 300;
return Math.floor(interval);
},
/* The URL actually used when the item is opened. Prefers the local URL only once it
has been confirmed reachable from the browser, otherwise uses the regular URL. */
effectiveUrl() {
if (this.hasLocalUrl && this.localUrlReachable === true) return this.item.localUrl;
return this.url || this.item.url;
},
/* Convert config target value, into HTML anchor target attribute */
anchorTarget() {
if (this.isEditMode) return '_self';
Expand All @@ -122,7 +151,7 @@ export default {
/* Get href for anchor, if not in edit mode, or opening in modal/ workspace */
hyperLinkHref() {
const nothing = '#';
const url = this.url || this.item.url || nothing;
const url = this.effectiveUrl || nothing;
if (this.isEditMode) return nothing;
const noAnchorNeeded = ['modal', 'workspace', 'clipboard', 'newwindow'];
return noAnchorNeeded.includes(this.accumulatedTarget) ? nothing : url;
Expand Down Expand Up @@ -210,9 +239,47 @@ export default {
});
}
},
/* Probes the configured local URL from the browser to decide if it's reachable.
Runs in the background (never blocks a click): the result feeds `effectiveUrl`,
which is already bound to the anchor's href by the time the user clicks.
Uses a no-cors fetch so cross-origin LAN services don't trip CORS — we only
care whether the request settles (reachable) or aborts/errors (unreachable). */
probeLocalUrl() {
if (!this.hasLocalUrl) return;
const target = this.item.localUrl.trim();
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.localUrlProbeTimeout);
fetch(target, {
mode: 'no-cors',
cache: 'no-store',
signal: controller.signal,
})
.then(() => { this.localUrlReachable = true; })
.catch(() => { this.localUrlReachable = false; })
.finally(() => clearTimeout(timer));
},
/* Starts local-URL probing: once now, on tab re-focus, and optionally on an interval */
startLocalUrlChecks() {
if (!this.hasLocalUrl) return;
this.probeLocalUrl();
if (this.localUrlCheckInterval > 0) {
this.localUrlIntervalId = setInterval(this.probeLocalUrl, this.localUrlCheckInterval * 1000);
}
// Re-probe when the tab becomes visible again (e.g. user switched networks)
document.addEventListener('visibilitychange', this.onVisibilityProbe);
},
/* Re-probe when the page regains visibility, so a network change is picked up */
onVisibilityProbe() {
if (document.visibilityState === 'visible') this.probeLocalUrl();
},
/* Tears down local-URL probing timers and listeners */
stopLocalUrlChecks() {
if (this.localUrlIntervalId) clearInterval(this.localUrlIntervalId);
document.removeEventListener('visibilitychange', this.onVisibilityProbe);
},
/* Called when an item is clicked, manages the opening of modal & resets the search field */
itemClicked(e) {
const url = this.url || this.item.url;
const url = this.effectiveUrl;
if (this.isEditMode) {
// If in edit mode, open settings, and don't launch app
e.preventDefault();
Expand Down Expand Up @@ -247,7 +314,7 @@ export default {
},
/* Open item, using specified method */
launchItem(method, link) {
const url = link || this.item.url;
const url = link || this.effectiveUrl;
this.contextMenuOpen = false;
switch (method) {
case 'newtab':
Expand Down
17 changes: 17 additions & 0 deletions src/utils/config/ConfigSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,23 @@
"type": "string",
"description": "The destination to navigate to when item is clicked, expressed as a valid URL, IP or hostname"
},
"localUrl": {
"title": "Local URL",
"type": "string",
"description": "Optional alternative URL preferred when it is reachable from your browser (e.g. a LAN address). On load Dashy probes this URL; if it responds, clicking the item opens it, otherwise it falls back to the Service URL. Useful for local-vs-remote access (Tailscale, VPN, etc)"
},
"localUrlTimeout": {
"title": "Local URL Probe Timeout",
"type": "number",
"default": 1500,
"description": "Milliseconds to wait for the Local URL reachability probe before giving up and using the Service URL. Clamped between 300 and 5000. The probe runs in the background, so it never delays clicks"
},
"localUrlCheckInterval": {
"title": "Local URL Re-check Interval",
"type": "number",
"default": 0,
"description": "Seconds between background re-checks of the Local URL. 0 = only check on page load and when the tab regains focus. Clamped to 300 max"
},
"displayData": {
"title": "Display Data",
"type": "object",
Expand Down