api-client/packages/hoppscotch-selfhost-web/src/main.ts
Shreyas 9861ee84ad
feat(desktop): settings phase 0 - infra and update check (#6172)
Co-authored-by: VicenzoMF <81040684+VicenzoMF@users.noreply.github.com>
2026-04-28 00:36:06 +05:30

423 lines
12 KiB
TypeScript

import { nextTick, ref, watch } from "vue"
import { emit, listen } from "@tauri-apps/api/event"
import { createHoppApp } from "@hoppscotch/common"
import { useSettingStatic } from "@hoppscotch/common/composables/settings"
import { getKernelMode } from "@hoppscotch/kernel"
import { def as stdBackendDef } from "@hoppscotch/common/platform/std/backend"
// Platform imports
import { def as webAuth } from "@app/platform/auth/web"
import { def as webEnvironments } from "@app/platform/environments/web"
import { def as webCollections } from "@app/platform/collections/web"
import { def as webSettings } from "@app/platform/settings/web"
import { def as webHistory } from "@app/platform/history/web"
import { def as desktopAuth } from "@app/platform/auth/desktop"
import { def as desktopEnvironments } from "@app/platform/environments/desktop"
import { def as desktopCollections } from "@app/platform/collections/desktop"
import { def as desktopSettings } from "@app/platform/settings/desktop"
import { def as desktopHistory } from "@app/platform/history/desktop"
// Std platform
import { def as webInstance } from "@app/platform/instance/web"
import { def as desktopInstance } from "@app/platform/instance/desktop"
import { stdFooterItems } from "@hoppscotch/common/platform/std/ui/footerItem"
import { stdSupportOptionItems } from "@hoppscotch/common/platform/std/ui/supportOptionsItem"
import { InfraPlatform } from "@app/platform/infra/infra.platform"
import { kernelIO } from "@hoppscotch/common/platform/std/kernel-io"
import { HeaderDownloadableLinksService } from "@app/services/headerDownloadableLinks.service"
import DesktopSettingsSection from "@hoppscotch/common/components/settings/Desktop.vue"
// Std interceptors
import { NativeKernelInterceptorService } from "@hoppscotch/common/platform/std/kernel-interceptors/native"
import { AgentKernelInterceptorService } from "@hoppscotch/common/platform/std/kernel-interceptors/agent"
import { ProxyKernelInterceptorService } from "@hoppscotch/common/platform/std/kernel-interceptors/proxy"
import { ExtensionKernelInterceptorService } from "@hoppscotch/common/platform/std/kernel-interceptors/extension"
import { BrowserKernelInterceptorService } from "@hoppscotch/common/platform/std/kernel-interceptors/browser"
const PLATFORM_CONFIG = {
web: {
auth: webAuth,
environments: webEnvironments,
collections: webCollections,
settings: webSettings,
history: webHistory,
instance: webInstance,
interceptors: [
BrowserKernelInterceptorService,
ProxyKernelInterceptorService,
AgentKernelInterceptorService,
ExtensionKernelInterceptorService,
],
defaultInterceptor: "browser",
menuItems: stdFooterItems,
supportItems: stdSupportOptionItems,
cookiesEnabled: false,
},
desktop: {
auth: desktopAuth,
environments: desktopEnvironments,
collections: desktopCollections,
settings: desktopSettings,
history: desktopHistory,
instance: desktopInstance,
interceptors: [
NativeKernelInterceptorService,
ProxyKernelInterceptorService,
],
defaultInterceptor: "native",
menuItems: stdFooterItems,
supportItems: stdSupportOptionItems,
cookiesEnabled: true,
},
}
const headerPaddingLeft = ref("0px")
const headerPaddingTop = ref("0px")
function setupDesktopUI() {
headerPaddingTop.value = "0px"
headerPaddingLeft.value = "80px"
listen("will-enter-fullscreen", () => {
headerPaddingTop.value = "0px"
headerPaddingLeft.value = "0px"
})
listen("will-exit-fullscreen", () => {
headerPaddingTop.value = "0px"
headerPaddingLeft.value = "80px"
})
window.addEventListener(
"keydown",
(e) => {
if (e.key === "Backspace" && !isTextInput(e.target)) {
e.preventDefault()
}
},
true
)
watch(
useSettingStatic("BG_COLOR")[0],
async () => {
await nextTick()
await emit("hopp-bg-changed")
},
{ immediate: true }
)
}
function isTextInput(target: EventTarget | null): boolean {
if (target instanceof HTMLInputElement) {
const textTypes = [
"text",
"email",
"password",
"number",
"search",
"tel",
"url",
"textarea",
]
return textTypes.includes(target.type)
}
return (
target instanceof HTMLTextAreaElement ||
(target instanceof HTMLElement && target.isContentEditable)
)
}
async function initApp() {
const platform = getKernelMode()
const config = PLATFORM_CONFIG[platform]
if (platform === "desktop") {
setupDesktopUI()
}
await createHoppApp("#app", {
ui: {
additionalFooterMenuItems: config.menuItems,
additionalSupportOptionsMenuItems: config.supportItems,
// Desktop-only. Renders the "Desktop" block in the shared settings
// page. The component lives in common so every shell that builds a
// Tauri desktop target can register it the same way. Web builds pass
// `undefined` here and the settings page renders without the block.
additionalSettingsSections:
platform === "desktop" ? [DesktopSettingsSection] : undefined,
appHeader: {
paddingLeft: headerPaddingLeft,
paddingTop: headerPaddingTop,
},
},
auth: config.auth,
kernelIO,
instance: config.instance,
sync: {
environments: config.environments,
collections: config.collections,
settings: config.settings,
history: config.history,
},
kernelInterceptors: {
default: config.defaultInterceptor,
interceptors: config.interceptors.map((service) => ({
type: "service" as const,
service,
})),
},
platformFeatureFlags: {
exportAsGIST: false,
hasTelemetry: false,
cookiesEnabled: config.cookiesEnabled,
promptAsUsingCookies: false,
hasCookieBasedAuth: platform === "web",
},
limits: {
collectionImportSizeLimit: 50,
},
infra: InfraPlatform,
backend: stdBackendDef,
additionalLinks: [HeaderDownloadableLinksService],
addedServices: [],
})
if (platform === "desktop") {
const ALLOWED_DROP_SELECTORS = [
'[draggable="true"]',
".draggable-content",
".draggable-handle",
".sortable-ghost",
".sortable-drag",
".sortable-chosen",
".vue-draggable",
'input[type="file"]',
'label[for*="attachment"]',
".file-chips-container",
".file-chips-wrapper",
".cm-editor",
".cm-content",
".cm-scroller",
".ace_editor",
"[data-allow-drop]",
".drop-zone",
"[ondrop]",
"[data-drop-handler]",
].join(", ")
const isAllowedDropTarget = (target: EventTarget | null): boolean => {
if (!target || !(target instanceof HTMLElement)) {
return false
}
if (target.closest(ALLOWED_DROP_SELECTORS)) {
return true
}
const element = target as any
if (element._vei?.onDrop || element.__vueListeners__?.drop) {
return true
}
return false
}
document.addEventListener(
"drop",
(e) => {
if (!isAllowedDropTarget(e.target)) {
e.preventDefault()
e.stopPropagation()
}
},
false
)
document.addEventListener(
"dragover",
(e) => {
e.preventDefault()
},
false
)
document.addEventListener(
"dragstart",
(e) => {
if (!e.target || !(e.target instanceof HTMLElement)) {
return
}
const target = e.target as HTMLElement
if (
!target.draggable &&
!target.closest('[draggable="true"], .draggable-content')
) {
e.preventDefault()
}
},
false
)
window.addEventListener(
"keydown",
function (e) {
// Prevent backspace navigation
// NOTE: Only for "non-text" inputs
if (e.key === "Backspace" && !isTextInput(e.target)) {
e.preventDefault()
return
}
const isCtrlOrCmd = e.ctrlKey || e.metaKey
let shortcutEvent: string | null = null
if (isCtrlOrCmd && !e.shiftKey && !e.altKey && e.code === "KeyQ") {
// Ctrl/Cmd + Q - Quit Application
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
shortcutEvent = "ctrl-q"
} else if (
isCtrlOrCmd &&
!e.shiftKey &&
!e.altKey &&
e.code === "KeyT"
) {
// Ctrl/Cmd + T - New Tab
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
shortcutEvent = "ctrl-t"
} else if (
isCtrlOrCmd &&
!e.shiftKey &&
!e.altKey &&
e.code === "KeyW"
) {
// Ctrl/Cmd + W - Close Tab
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
shortcutEvent = "ctrl-w"
} else if (
isCtrlOrCmd &&
e.shiftKey &&
!e.altKey &&
e.code === "KeyT"
) {
// Ctrl/Cmd + Shift + T - Reopen Tab
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
shortcutEvent = "ctrl-shift-t"
} else if (
isCtrlOrCmd &&
!e.shiftKey &&
e.altKey &&
e.key === "ArrowRight"
) {
// Ctrl/Cmd + Alt + Right - Next Tab
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
shortcutEvent = "ctrl-alt-right"
} else if (
isCtrlOrCmd &&
!e.shiftKey &&
e.altKey &&
e.key === "ArrowLeft"
) {
// Ctrl/Cmd + Alt + Left - Previous Tab
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
shortcutEvent = "ctrl-alt-left"
} else if (
isCtrlOrCmd &&
!e.shiftKey &&
e.altKey &&
(e.code === "Digit9" ||
(e.code === "Numpad9" && e.getModifierState("NumLock")))
) {
// Ctrl/Cmd + Alt + 9 - First Tab
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
shortcutEvent = "ctrl-alt-9"
} else if (
isCtrlOrCmd &&
!e.shiftKey &&
e.altKey &&
(e.code === "Digit0" ||
(e.code === "Numpad0" && e.getModifierState("NumLock")))
) {
// Ctrl/Cmd + Alt + 0 - Last Tab
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
shortcutEvent = "ctrl-alt-0"
} else if (
isCtrlOrCmd &&
!e.shiftKey &&
e.altKey &&
e.code === "KeyU"
) {
// Ctrl/Cmd + Alt + U - Focus URL Bar
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
shortcutEvent = "ctrl-alt-u"
} else if (
isCtrlOrCmd &&
!e.shiftKey &&
e.altKey &&
e.code === "BracketRight"
) {
// Ctrl/Cmd + Alt + ] - MRU Tab Switch
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
shortcutEvent = "ctrl-alt-]"
} else if (
isCtrlOrCmd &&
!e.shiftKey &&
e.altKey &&
e.code === "BracketLeft"
) {
// Ctrl/Cmd + Alt + [ - MRU Tab Switch (Reverse)
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
shortcutEvent = "ctrl-alt-["
}
if (shortcutEvent) {
setTimeout(() => {
emit("hoppscotch_desktop_shortcut", shortcutEvent).catch(
(error) => {
console.error("Failed to emit shortcut event:", error)
}
)
}, 0)
}
},
true
)
}
}
initApp().catch(console.error)