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
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
**`localUrlTimeout`** | `number` | _Optional_ | Milliseconds to wait for the `localUrl` reachability probe before giving up and using `url`. 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. `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
2 changes: 1 addition & 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
83 changes: 80 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
localUrlReachable: undefined, // Locally reachable? unset if not yet probed, else true/false
localUrlIntervalId: undefined, // local-url re-check setInterval() id
localUrlController: undefined, // AbortController for the in-flight probe
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,49 @@ export default {
});
}
},
/* Probes the configured local URL from the browser to decide if it's reachable */
probeLocalUrl() {
if (!this.hasLocalUrl) return;
const target = this.item.localUrl.trim();
if (this.localUrlController) this.localUrlController.abort();
const controller = new AbortController();
this.localUrlController = controller;
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);
if (this.localUrlController === controller) this.localUrlController = undefined;
});
},
/* 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, listeners and any in-flight probe */
stopLocalUrlChecks() {
if (this.localUrlIntervalId) clearInterval(this.localUrlIntervalId);
if (this.localUrlController) this.localUrlController.abort();
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 +316,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 Expand Up @@ -320,4 +389,12 @@ export default {
} catch { /* ignore corrupt localStorage */ }
},
},
mounted() {
// If an alternative local URL is set, probe its reachability in the background
if (this.hasLocalUrl) this.startLocalUrlChecks();
},
beforeUnmount() {
// Stop local-URL probing timers, listeners and any in-flight probe
this.stopLocalUrlChecks();
},
};
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)"
},
"localUrlTimeout": {
"title": "Local URL Probe Timeout",
"type": "number",
"default": 1500,
"description": "For local URLs, ms to wait for the Local URL reachability probe before falling back to normal URL"
},
"localUrlCheckInterval": {
"title": "Local URL Re-check Interval",
"type": "number",
"default": 0,
"description": "For local URLs, seconds between background re-checks of the Local URL. 0 = only check on page load"
},
"displayData": {
"title": "Display Data",
"type": "object",
Expand Down