Compare commits
28 Commits
refactor/w
...
feat/deskt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e43f26563 | ||
|
|
8387e67fd6 | ||
|
|
95948b49b2 | ||
|
|
7811f3b53e | ||
|
|
2757722fd0 | ||
|
|
dbbeb54754 | ||
|
|
9b3d8046bf | ||
|
|
a471b250a3 | ||
|
|
39b2dab3c9 | ||
|
|
cdf9c31935 | ||
|
|
a1de73906a | ||
|
|
071a6330e4 | ||
|
|
654886b871 | ||
|
|
b29a62009b | ||
|
|
160b1ae406 | ||
|
|
ab52fcba6d | ||
|
|
636133168a | ||
|
|
2a4f6280bc | ||
|
|
05c8a1aa30 | ||
|
|
20ba4bed5d | ||
|
|
4587cee189 | ||
|
|
e2b15cedd4 | ||
|
|
7fff08c142 | ||
|
|
3f1ca8f20b | ||
|
|
d068da1341 | ||
|
|
3e2d029ee3 | ||
|
|
9ff42dd3a4 | ||
|
|
6221e125b1 |
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "Add",
|
||||
"autoscroll": "Autoscroll",
|
||||
"cancel": "Cancel",
|
||||
"choose_file": "Choose a file",
|
||||
@@ -54,9 +55,28 @@
|
||||
"new": "Add new",
|
||||
"star": "Add star"
|
||||
},
|
||||
"cookies": {
|
||||
"modal": {
|
||||
"new_domain_name": "New domain name",
|
||||
"set": "Set a cookie",
|
||||
"cookie_string": "Cookie string",
|
||||
"enter_cookie_string": "Enter cookie string",
|
||||
"cookie_name": "Name",
|
||||
"cookie_value": "Value",
|
||||
"cookie_path": "Path",
|
||||
"cookie_expires": "Expires",
|
||||
"managed_tab": "Managed",
|
||||
"raw_tab": "Raw",
|
||||
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
|
||||
"empty_domains": "Domain list is empty",
|
||||
"empty_domain": "Domain is empty",
|
||||
"no_cookies_in_domain": "No cookies set for this domain"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"chat_with_us": "Chat with us",
|
||||
"contact_us": "Contact us",
|
||||
"cookies": "Cookies",
|
||||
"copy": "Copy",
|
||||
"copy_user_id": "Copy User Auth Token",
|
||||
"developer_option": "Developer options",
|
||||
@@ -764,7 +784,7 @@
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"show":"Show",
|
||||
"show": "Show",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"acorn-walk": "^8.2.0",
|
||||
"axios": "^1.4.0",
|
||||
"buffer": "^6.0.3",
|
||||
"cookie-es": "^1.0.0",
|
||||
"dioc": "workspace:^",
|
||||
"esprima": "^4.0.1",
|
||||
"events": "^3.3.0",
|
||||
@@ -76,6 +77,8 @@
|
||||
"process": "^0.11.10",
|
||||
"qs": "^6.11.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"set-cookie-parser-es": "^1.0.5",
|
||||
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
|
||||
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
|
||||
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
|
||||
@@ -98,7 +101,8 @@
|
||||
"wonka": "^6.3.4",
|
||||
"workbox-window": "^7.0.0",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"yargs-parser": "^21.1.1"
|
||||
"yargs-parser": "^21.1.1",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||
|
||||
19
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -1,11 +1,11 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
||||
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
||||
@@ -58,6 +58,8 @@ declare module 'vue' {
|
||||
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
|
||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
|
||||
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
|
||||
Environments: typeof import('./components/environments/index.vue')['default']
|
||||
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
|
||||
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
||||
@@ -90,13 +92,11 @@ declare module 'vue' {
|
||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
|
||||
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
|
||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
||||
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
||||
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
|
||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||
@@ -143,7 +143,6 @@ declare module 'vue' {
|
||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
||||
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
|
||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||
@@ -153,10 +152,8 @@ declare module 'vue' {
|
||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
|
||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||
@@ -189,7 +186,6 @@ declare module 'vue' {
|
||||
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
|
||||
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
|
||||
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
|
||||
SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
|
||||
SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default']
|
||||
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
|
||||
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
||||
@@ -222,4 +218,5 @@ declare module 'vue' {
|
||||
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
||||
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
<AppInterceptor />
|
||||
</template>
|
||||
</tippy>
|
||||
<HoppButtonSecondary
|
||||
v-if="platform.platformFeatureFlags.cookiesEnabled ?? false"
|
||||
:label="t('app.cookies')"
|
||||
:icon="IconCookie"
|
||||
@click="showCookiesModal = true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<tippy
|
||||
@@ -195,12 +201,17 @@
|
||||
:show="showDeveloperOptions"
|
||||
@hide-modal="showDeveloperOptions = false"
|
||||
/>
|
||||
<CookiesAllModal
|
||||
:show="showCookiesModal"
|
||||
@hide-modal="showCookiesModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { version } from "~/../package.json"
|
||||
import IconCookie from "~icons/lucide/cookie"
|
||||
import IconSidebar from "~icons/lucide/sidebar"
|
||||
import IconZap from "~icons/lucide/zap"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
@@ -223,7 +234,9 @@ import { invokeAction } from "@helpers/actions"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const showDeveloperOptions = ref(false)
|
||||
const showCookiesModal = ref(false)
|
||||
|
||||
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
||||
const SIDEBAR = useSetting("SIDEBAR")
|
||||
|
||||
@@ -258,7 +258,7 @@ const importFromJSON = () => {
|
||||
inputChooseFileToImportFrom.value.value = ""
|
||||
}
|
||||
|
||||
const exportJSON = () => {
|
||||
const exportJSON = async () => {
|
||||
const dataToWrite = collectionJson.value
|
||||
|
||||
const parsedCollections = JSON.parse(dataToWrite)
|
||||
@@ -268,24 +268,32 @@ const exportJSON = () => {
|
||||
}
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
platform?.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "gql",
|
||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/json",
|
||||
suggestedFilename: filename,
|
||||
filters: [
|
||||
{
|
||||
name: "Hoppscotch Collection JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// TODO: get uri from meta
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
toast.success(t("state.download_started").toString())
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
platform?.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "gql",
|
||||
})
|
||||
|
||||
toast.success(t("state.download_started").toString())
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1866,28 +1866,25 @@ const getJSONCollection = async () => {
|
||||
* @param collectionJSON - JSON string of the collection
|
||||
* @param name - Name of the collection set as the file name
|
||||
*/
|
||||
const initializeDownloadCollection = (
|
||||
const initializeDownloadCollection = async (
|
||||
collectionJSON: string,
|
||||
name: string | null
|
||||
) => {
|
||||
const file = new Blob([collectionJSON], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: collectionJSON,
|
||||
contentType: "application/json",
|
||||
suggestedFilename: `${name ?? "collection"}.json`,
|
||||
filters: [
|
||||
{
|
||||
name: "Hoppscotch Collection JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (name) {
|
||||
a.download = `${name}.json`
|
||||
} else {
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
toast.success(t("state.download_started").toString())
|
||||
}
|
||||
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
toast.success(t("state.download_started").toString())
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1916,11 +1913,14 @@ const exportData = async (
|
||||
exportLoading.value = false
|
||||
return
|
||||
},
|
||||
(coll) => {
|
||||
async (coll) => {
|
||||
const hoppColl = teamCollToHoppRESTColl(coll)
|
||||
const collectionJSONString = JSON.stringify(hoppColl)
|
||||
|
||||
initializeDownloadCollection(collectionJSONString, hoppColl.name)
|
||||
await initializeDownloadCollection(
|
||||
collectionJSONString,
|
||||
hoppColl.name
|
||||
)
|
||||
exportLoading.value = false
|
||||
}
|
||||
)
|
||||
|
||||
269
packages/hoppscotch-common/src/components/cookies/AllModal.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('app.cookies')"
|
||||
aria-modal="true"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="!currentInterceptorSupportsCookies"
|
||||
:text="t('cookies.modal.interceptor_no_support')"
|
||||
>
|
||||
<AppInterceptor class="p-2 border rounded border-dividerLight" />
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else class="flex flex-col">
|
||||
<div
|
||||
class="flex bg-primary space-x-2 border-b sticky border-dividerLight -mx-4 px-4 py-4 -mt-4"
|
||||
style="top: calc(-1 * var(--line-height-body))"
|
||||
>
|
||||
<HoppSmartInput
|
||||
v-model="newDomainText"
|
||||
class="flex-1"
|
||||
:placeholder="t('cookies.modal.new_domain_name')"
|
||||
@keyup.enter="addNewDomain"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
filled
|
||||
:label="t('action.add')"
|
||||
@click="addNewDomain"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<HoppSmartPlaceholder
|
||||
v-if="workingCookieJar.size === 0"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('cookies.modal.empty_domains')}`"
|
||||
:text="t('cookies.modal.empty_domains')"
|
||||
class="mt-6"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
<div
|
||||
v-for="[domain, entries] in workingCookieJar.entries()"
|
||||
v-else
|
||||
:key="domain"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<div class="flex items-center justify-between flex-1">
|
||||
<label for="cookiesList" class="p-4">
|
||||
{{ domain }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.delete')"
|
||||
:icon="IconTrash2"
|
||||
@click="deleteDomain(domain)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('add.new')"
|
||||
:icon="IconPlus"
|
||||
@click="addCookieToDomain(domain)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border rounded border-divider">
|
||||
<div class="divide-y divide-dividerLight">
|
||||
<div
|
||||
v-if="entries.length === 0"
|
||||
class="flex flex-col gap-2 p-4 items-center"
|
||||
>
|
||||
{{ t("cookies.modal.no_cookies_in_domain") }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(entry, entryIndex) in entries"
|
||||
:key="`${entry}-${entryIndex}`"
|
||||
class="flex divide-x divide-dividerLight"
|
||||
>
|
||||
<input
|
||||
class="flex flex-1 px-4 py-2 bg-transparent"
|
||||
:value="entry"
|
||||
readonly
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.edit')"
|
||||
:icon="IconEdit"
|
||||
@click="editCookie(domain, entryIndex, entry)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
@click="deleteCookie(domain, entryIndex)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="currentInterceptorSupportsCookies" #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
v-focus
|
||||
:label="t('action.save')"
|
||||
outline
|
||||
@click="saveCookieChanges"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="cancelCookieChanges"
|
||||
/>
|
||||
</span>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.clear_all')"
|
||||
outline
|
||||
filled
|
||||
@click="clearAllDomains"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
<CookiesEditCookie
|
||||
:show="!!showEditModalFor"
|
||||
:entry="showEditModalFor"
|
||||
@save-cookie="saveCookie"
|
||||
@hide-modal="showEditModalFor = null"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useService } from "dioc/vue"
|
||||
import { CookieJarService } from "~/services/cookie-jar.service"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { ref, watch, computed } from "vue"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { EditCookieConfig } from "./EditCookie.vue"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useToast } from "@composables/toast"
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
const toast = useToast()
|
||||
|
||||
const newDomainText = ref("")
|
||||
|
||||
const interceptorService = useService(InterceptorService)
|
||||
const cookieJarService = useService(CookieJarService)
|
||||
|
||||
const workingCookieJar = ref(cloneDeep(cookieJarService.cookieJar.value))
|
||||
|
||||
const currentInterceptorSupportsCookies = computed(() => {
|
||||
const currentInterceptor = interceptorService.currentInterceptor.value
|
||||
|
||||
if (!currentInterceptor) return true
|
||||
|
||||
return currentInterceptor.supportsCookies ?? false
|
||||
})
|
||||
|
||||
function addNewDomain() {
|
||||
if (newDomainText.value === "" || /^\s+$/.test(newDomainText.value)) {
|
||||
toast.error(`${t("cookies.modal.empty_domain")}`)
|
||||
return
|
||||
}
|
||||
|
||||
workingCookieJar.value.set(newDomainText.value, [])
|
||||
newDomainText.value = ""
|
||||
}
|
||||
|
||||
function deleteDomain(domain: string) {
|
||||
workingCookieJar.value.delete(domain)
|
||||
}
|
||||
|
||||
function addCookieToDomain(domain: string) {
|
||||
showEditModalFor.value = { type: "create", domain }
|
||||
}
|
||||
|
||||
function clearAllDomains() {
|
||||
workingCookieJar.value = new Map()
|
||||
toast.success(`${t("state.cleared")}`)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
workingCookieJar.value = cloneDeep(cookieJarService.cookieJar.value)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const showEditModalFor = ref<EditCookieConfig | null>(null)
|
||||
|
||||
function saveCookieChanges() {
|
||||
cookieJarService.cookieJar.value = workingCookieJar.value
|
||||
hideModal()
|
||||
}
|
||||
|
||||
function cancelCookieChanges() {
|
||||
hideModal()
|
||||
}
|
||||
|
||||
function editCookie(domain: string, entryIndex: number, cookieEntry: string) {
|
||||
showEditModalFor.value = {
|
||||
type: "edit",
|
||||
domain,
|
||||
entryIndex,
|
||||
currentCookieEntry: cookieEntry,
|
||||
}
|
||||
}
|
||||
|
||||
function deleteCookie(domain: string, entryIndex: number) {
|
||||
const entry = workingCookieJar.value.get(domain)
|
||||
|
||||
if (entry) {
|
||||
entry.splice(entryIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function saveCookie(cookie: string) {
|
||||
if (showEditModalFor.value?.type === "create") {
|
||||
const { domain } = showEditModalFor.value
|
||||
|
||||
const entry = workingCookieJar.value.get(domain)!
|
||||
entry.push(cookie)
|
||||
|
||||
showEditModalFor.value = null
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (showEditModalFor.value?.type !== "edit") return
|
||||
|
||||
const { domain, entryIndex } = showEditModalFor.value!
|
||||
|
||||
const entry = workingCookieJar.value.get(domain)
|
||||
|
||||
if (entry) {
|
||||
entry[entryIndex] = cookie
|
||||
}
|
||||
|
||||
showEditModalFor.value = null
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
195
packages/hoppscotch-common/src/components/cookies/EditCookie.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('cookies.modal.set')"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="border rounded border-dividerLight">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center justify-between pl-4">
|
||||
<label class="font-semibold truncate text-secondaryLight">
|
||||
{{ t("cookies.modal.cookie_string") }}
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="t('action.download_file')"
|
||||
:icon="downloadIcon"
|
||||
@click="downloadResponse"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyIcon"
|
||||
@click="copyResponse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-46">
|
||||
<div
|
||||
ref="cookieEditor"
|
||||
class="h-full border-t rounded-b border-dividerLight"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
v-focus
|
||||
:label="t('action.save')"
|
||||
outline
|
||||
@click="saveCookieChange"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="cancelCookieChange"
|
||||
/>
|
||||
</div>
|
||||
<span class="flex">
|
||||
<HoppButtonSecondary
|
||||
:icon="pasteIcon"
|
||||
:label="`${t('action.paste')}`"
|
||||
filled
|
||||
outline
|
||||
@click="handlePaste"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export type EditCookieConfig =
|
||||
| { type: "create"; domain: string }
|
||||
| {
|
||||
type: "edit"
|
||||
domain: string
|
||||
entryIndex: number
|
||||
currentCookieEntry: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useCodemirror } from "~/composables/codemirror"
|
||||
import { watch, ref, reactive } from "vue"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import IconClipboard from "~icons/lucide/clipboard"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import {
|
||||
useCopyResponse,
|
||||
useDownloadResponse,
|
||||
} from "~/composables/lens-actions"
|
||||
|
||||
// TODO: Build Managed Mode!
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
|
||||
entry: EditCookieConfig | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "save-cookie", cookie: string): void
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const cookieEditor = ref<HTMLElement>()
|
||||
const rawCookieString = ref("")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
cookieEditor,
|
||||
rawCookieString,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "text/plain",
|
||||
placeholder: `${t("cookies.modal.enter_cookie_string")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
environmentHighlights: false,
|
||||
})
|
||||
)
|
||||
|
||||
const pasteIcon = refAutoReset<typeof IconClipboard | typeof IconCheck>(
|
||||
IconClipboard,
|
||||
1000
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.entry,
|
||||
() => {
|
||||
if (!props.entry) return
|
||||
|
||||
if (props.entry.type === "create") {
|
||||
rawCookieString.value = ""
|
||||
return
|
||||
}
|
||||
|
||||
rawCookieString.value = props.entry.currentCookieEntry
|
||||
}
|
||||
)
|
||||
|
||||
function hideModal() {
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
function cancelCookieChange() {
|
||||
hideModal()
|
||||
}
|
||||
|
||||
async function handlePaste() {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (text) {
|
||||
rawCookieString.value = text
|
||||
pasteIcon.value = IconCheck
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to copy: ", e)
|
||||
toast.error(t("profile.no_permission").toString())
|
||||
}
|
||||
}
|
||||
|
||||
function saveCookieChange() {
|
||||
emit("save-cookie", rawCookieString.value)
|
||||
}
|
||||
|
||||
const { copyIcon, copyResponse } = useCopyResponse(rawCookieString)
|
||||
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
"",
|
||||
rawCookieString
|
||||
)
|
||||
|
||||
function clearContent() {
|
||||
rawCookieString.value = ""
|
||||
}
|
||||
</script>
|
||||
@@ -375,7 +375,7 @@ const importFromPostman = ({
|
||||
importFromHoppscotch(environments)
|
||||
}
|
||||
|
||||
const exportJSON = () => {
|
||||
const exportJSON = async () => {
|
||||
const dataToWrite = environmentJson.value
|
||||
|
||||
const parsedCollections = JSON.parse(dataToWrite)
|
||||
@@ -385,19 +385,27 @@ const exportJSON = () => {
|
||||
}
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
// TODO: get uri from meta
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
toast.success(t("state.download_started").toString())
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/json",
|
||||
suggestedFilename: filename,
|
||||
filters: [
|
||||
{
|
||||
name: "JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
toast.success(t("state.download_started").toString())
|
||||
}
|
||||
}
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
|
||||
@@ -59,6 +59,7 @@ import { useToast } from "@composables/toast"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -111,21 +112,31 @@ const copyResponse = (str: string) => {
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
}
|
||||
|
||||
const downloadResponse = (str: string) => {
|
||||
const downloadResponse = async (str: string) => {
|
||||
const dataToWrite = str
|
||||
const file = new Blob([dataToWrite!], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadResponseIcon.value = IconCheck
|
||||
toast.success(`${t("state.download_started")}`)
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
|
||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/json",
|
||||
suggestedFilename: filename,
|
||||
filters: [
|
||||
{
|
||||
name: "JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
downloadResponseIcon.value = IconCheck
|
||||
toast.success(`${t("state.download_started")}`)
|
||||
}
|
||||
}
|
||||
|
||||
defineActionHandler(
|
||||
|
||||
@@ -202,6 +202,7 @@ import {
|
||||
schemaString,
|
||||
subscriptionFields,
|
||||
} from "~/helpers/graphql/connection"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
type NavigationTabs = "history" | "collection" | "docs" | "schema"
|
||||
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
|
||||
@@ -372,21 +373,33 @@ useCodemirror(
|
||||
})
|
||||
)
|
||||
|
||||
const downloadSchema = () => {
|
||||
const dataToWrite = JSON.stringify(schemaString.value, null, 2)
|
||||
const downloadSchema = async () => {
|
||||
const dataToWrite = schemaString.value
|
||||
const file = new Blob([dataToWrite], { type: "application/graphql" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.graphql`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadSchemaIcon.value = IconCheck
|
||||
toast.success(`${t("state.download_started")}`)
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
|
||||
const filename = `${
|
||||
url.split("/").pop()!.split("#")[0].split("?")[0]
|
||||
}.graphql`
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/graphql",
|
||||
suggestedFilename: filename,
|
||||
filters: [
|
||||
{
|
||||
name: "GraphQL Schema File",
|
||||
extensions: ["graphql"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
downloadSchemaIcon.value = IconCheck
|
||||
toast.success(`${t("state.download_started")}`)
|
||||
}
|
||||
}
|
||||
|
||||
const copySchema = () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useI18n } from "./i18n"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { copyToClipboard } from "@helpers/utils/clipboard"
|
||||
import { HoppRESTResponse } from "@helpers/types/HoppRESTResponse"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
export function useCopyResponse(responseBodyText: Ref<any>) {
|
||||
const toast = useToast()
|
||||
@@ -40,15 +41,14 @@ export function useDownloadResponse(
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
|
||||
const downloadResponse = () => {
|
||||
const downloadResponse = async () => {
|
||||
const dataToWrite = responseBody.value
|
||||
const file = new Blob([dataToWrite], { type: contentType })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
// TODO: get uri from meta
|
||||
a.download = pipe(
|
||||
// Guess extension and filename
|
||||
const file = new Blob([dataToWrite], { type: contentType })
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
const filename = pipe(
|
||||
url,
|
||||
S.split("/"),
|
||||
RNEA.last,
|
||||
@@ -58,15 +58,24 @@ export function useDownloadResponse(
|
||||
RNEA.head
|
||||
)
|
||||
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadIcon.value = IconCheck
|
||||
toast.success(`${t("state.download_started")}`)
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
console.log(filename)
|
||||
|
||||
// TODO: Look at the mime type and determine extension ?
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: contentType,
|
||||
suggestedFilename: filename,
|
||||
})
|
||||
|
||||
// Assume success if unknown as we cannot determine
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
downloadIcon.value = IconCheck
|
||||
toast.success(`${t("state.download_started")}`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
downloadIcon,
|
||||
downloadResponse,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const getEnvironmentJson = (
|
||||
environmentObj: TeamEnvironment | Environment,
|
||||
@@ -32,17 +33,24 @@ export const exportAsJSON = (
|
||||
if (!dataToWrite) return false
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
// Extracts the path from url, removes fragment identifier and query parameters if any, appends the ".json" extension, and assigns it
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}, 0)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/json",
|
||||
// Extracts the path from url, removes fragment identifier and query parameters if any, appends the ".json" extension, and assigns it
|
||||
suggestedFilename: `${
|
||||
url.split("/").pop()!.split("#")[0].split("?")[0]
|
||||
}.json`,
|
||||
filters: [
|
||||
{
|
||||
name: "JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ import { StorageLike, watchDebounced } from "@vueuse/core"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
import { z } from "zod"
|
||||
import { CookieJarService } from "~/services/cookie-jar.service"
|
||||
import { watch } from "vue"
|
||||
|
||||
function checkAndMigrateOldSettings() {
|
||||
if (window.localStorage.getItem("selectedEnvIndex")) {
|
||||
@@ -182,6 +185,35 @@ function setupHistoryPersistence() {
|
||||
})
|
||||
}
|
||||
|
||||
const cookieSchema = z.record(z.array(z.string()))
|
||||
|
||||
function setupCookiesPersistence() {
|
||||
const cookieJarService = getService(CookieJarService)
|
||||
|
||||
try {
|
||||
const cookieData = JSON.parse(
|
||||
window.localStorage.getItem("cookieJar") || "{}"
|
||||
)
|
||||
|
||||
const parseResult = cookieSchema.safeParse(cookieData)
|
||||
|
||||
if (parseResult.success) {
|
||||
for (const domain in parseResult.data) {
|
||||
cookieJarService.bulkApplyCookiesToDomain(
|
||||
parseResult.data[domain],
|
||||
domain
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
watch(cookieJarService.cookieJar, (cookieJar) => {
|
||||
const data = JSON.stringify(Object.fromEntries(cookieJar.entries()))
|
||||
|
||||
window.localStorage.setItem("cookieJar", data)
|
||||
})
|
||||
}
|
||||
|
||||
function setupCollectionsPersistence() {
|
||||
const restCollectionData = JSON.parse(
|
||||
window.localStorage.getItem("collections") || "[]"
|
||||
@@ -382,6 +414,8 @@ export function setupLocalPersistence() {
|
||||
setupSocketIOPersistence()
|
||||
setupSSEPersistence()
|
||||
setupMQTTPersistence()
|
||||
|
||||
setupCookiesPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,12 +9,14 @@ import { AnalyticsPlatformDef } from "./analytics"
|
||||
import { InterceptorsPlatformDef } from "./interceptors"
|
||||
import { HoppModule } from "~/modules"
|
||||
import { InspectorsPlatformDef } from "./inspectors"
|
||||
import { IOPlatformDef } from "./io"
|
||||
|
||||
export type PlatformDef = {
|
||||
ui?: UIPlatformDef
|
||||
addedHoppModules?: HoppModule[]
|
||||
auth: AuthPlatformDef
|
||||
analytics?: AnalyticsPlatformDef
|
||||
io: IOPlatformDef
|
||||
sync: {
|
||||
environments: EnvironmentsPlatformDef
|
||||
collections: CollectionsPlatformDef
|
||||
@@ -27,6 +29,12 @@ export type PlatformDef = {
|
||||
platformFeatureFlags: {
|
||||
exportAsGIST: boolean
|
||||
hasTelemetry: boolean
|
||||
|
||||
/**
|
||||
* Whether the platform supports cookies (affects whether the cookies footer item is shown)
|
||||
* If a value is not given, then the value is assumed to be false
|
||||
*/
|
||||
cookiesEnabled?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
84
packages/hoppscotch-common/src/platform/io.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Defines how to save a file to the user's filesystem.
|
||||
*/
|
||||
export type SaveFileWithDialogOptions = {
|
||||
/**
|
||||
* The data to be saved
|
||||
*/
|
||||
data: string | ArrayBuffer
|
||||
|
||||
/**
|
||||
* The suggested filename for the file. This name will be shown in the
|
||||
* save dialog by default when a save is initiated.
|
||||
*/
|
||||
suggestedFilename: string
|
||||
|
||||
/**
|
||||
* The content type mime type of the data to be saved.
|
||||
*
|
||||
* NOTE: The usage of this data might be platform dependent.
|
||||
* For example, this field is used in the web, but not in the desktop app.
|
||||
*/
|
||||
contentType: string
|
||||
|
||||
/**
|
||||
* Defines the filters (like in Windows, on the right side, where you can
|
||||
* select the file type) for the file dialog.
|
||||
*
|
||||
* NOTE: The usage of this data might be platform dependent.
|
||||
* For example, this field is used in the web, but not in the desktop app.
|
||||
*/
|
||||
filters?: Array<{
|
||||
/**
|
||||
* The name of the filter (in Windows, if the filter looks
|
||||
* like "Images (*.png, *.jpg)", the name would be "Images")
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* The array of extensions that are supported, without the dot.
|
||||
*/
|
||||
extensions: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export type SaveFileResponse =
|
||||
| {
|
||||
/**
|
||||
* The implementation was unable to determine the status of the save operation.
|
||||
* This cannot be considered a success or a failure and should be handled as an uncertainity.
|
||||
* The browser standard implementation (std) returns this value as there is no way to
|
||||
* check if the user downloaded the file or not.
|
||||
*/
|
||||
type: "unknown"
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The result is known and the user cancelled the save.
|
||||
*/
|
||||
type: "cancelled"
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The result is known and the user saved the file.
|
||||
*/
|
||||
type: "saved"
|
||||
|
||||
/**
|
||||
* The full path of where the file was saved
|
||||
*/
|
||||
path: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform definitions for how to handle IO operations.
|
||||
*/
|
||||
export type IOPlatformDef = {
|
||||
/**
|
||||
* Defines how to save a file to the user's filesystem.
|
||||
* The expected behaviour is for the browser to show a prompt to save the file.
|
||||
*/
|
||||
saveFileWithDialog: (
|
||||
opts: SaveFileWithDialogOptions
|
||||
) => Promise<SaveFileResponse>
|
||||
}
|
||||
37
packages/hoppscotch-common/src/platform/std/io.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { IOPlatformDef } from "../io"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as S from "fp-ts/string"
|
||||
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
|
||||
|
||||
/**
|
||||
* Implementation for how to handle IO operations in the browser.
|
||||
*/
|
||||
export const browserIODef: IOPlatformDef = {
|
||||
saveFileWithDialog(opts) {
|
||||
const file = new Blob([opts.data], { type: opts.contentType })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
a.href = url
|
||||
a.download = pipe(
|
||||
url,
|
||||
S.split("/"),
|
||||
RNEA.last,
|
||||
S.split("#"),
|
||||
RNEA.head,
|
||||
S.split("?"),
|
||||
RNEA.head
|
||||
)
|
||||
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
|
||||
// Browsers provide no way for us to know the save went successfully.
|
||||
return Promise.resolve({ type: "unknown" })
|
||||
},
|
||||
}
|
||||
@@ -72,6 +72,61 @@ describe("InterceptorService", () => {
|
||||
expect(service.currentInterceptorID.value).not.toEqual("unknown")
|
||||
})
|
||||
|
||||
it("currentInterceptor points to the instance of the currently selected interceptor", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const service = container.bind(InterceptorService)
|
||||
|
||||
const interceptor = {
|
||||
interceptorID: "test",
|
||||
name: () => "test interceptor",
|
||||
selectable: { type: "selectable" as const },
|
||||
runRequest: () => {
|
||||
throw new Error("not implemented")
|
||||
},
|
||||
}
|
||||
|
||||
service.registerInterceptor(interceptor)
|
||||
service.currentInterceptorID.value = "test"
|
||||
|
||||
expect(service.currentInterceptor.value).toBe(interceptor)
|
||||
})
|
||||
|
||||
it("currentInterceptor updates when the currentInterceptorID changes", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const service = container.bind(InterceptorService)
|
||||
|
||||
const interceptor = {
|
||||
interceptorID: "test",
|
||||
name: () => "test interceptor",
|
||||
selectable: { type: "selectable" as const },
|
||||
runRequest: () => {
|
||||
throw new Error("not implemented")
|
||||
},
|
||||
}
|
||||
|
||||
const interceptor_2 = {
|
||||
interceptorID: "test2",
|
||||
name: () => "test interceptor",
|
||||
selectable: { type: "selectable" as const },
|
||||
runRequest: () => {
|
||||
throw new Error("not implemented")
|
||||
},
|
||||
}
|
||||
|
||||
service.registerInterceptor(interceptor)
|
||||
service.registerInterceptor(interceptor_2)
|
||||
|
||||
service.currentInterceptorID.value = "test"
|
||||
|
||||
expect(service.currentInterceptor.value).toBe(interceptor)
|
||||
|
||||
service.currentInterceptorID.value = "test2"
|
||||
expect(service.currentInterceptor.value).not.toBe(interceptor)
|
||||
expect(service.currentInterceptor.value).toBe(interceptor_2)
|
||||
})
|
||||
|
||||
describe("registerInterceptor", () => {
|
||||
it("should register the interceptor", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Service } from "dioc"
|
||||
import { ref } from "vue"
|
||||
import { parseString as setCookieParse } from "set-cookie-parser-es"
|
||||
|
||||
export type CookieDef = {
|
||||
name: string
|
||||
value: string
|
||||
domain: string
|
||||
path: string
|
||||
expires: string
|
||||
}
|
||||
|
||||
export class CookieJarService extends Service {
|
||||
public static readonly ID = "COOKIE_JAR_SERVICE"
|
||||
|
||||
/**
|
||||
* The cookie jar that stores all relevant cookie info.
|
||||
* The keys correspond to the domain of the cookie.
|
||||
* The cookie strings are stored as an array of strings corresponding to the domain
|
||||
*/
|
||||
public cookieJar = ref(new Map<string, string[]>())
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
public parseSetCookieString(setCookieString: string) {
|
||||
return setCookieParse(setCookieString)
|
||||
}
|
||||
|
||||
public bulkApplyCookiesToDomain(cookies: string[], domain: string) {
|
||||
const existingDomainEntries = this.cookieJar.value.get(domain) ?? []
|
||||
existingDomainEntries.push(...cookies)
|
||||
|
||||
this.cookieJar.value.set(domain, existingDomainEntries)
|
||||
}
|
||||
|
||||
public getCookiesForURL(url: URL) {
|
||||
const relevantDomains = Array.from(this.cookieJar.value.keys()).filter(
|
||||
(domain) => url.hostname.endsWith(domain)
|
||||
)
|
||||
|
||||
return relevantDomains
|
||||
.flatMap((domain) => {
|
||||
// Assemble the list of cookie entries from all the relevant domains
|
||||
|
||||
const cookieStrings = this.cookieJar.value.get(domain)! // We know not nullable from how we filter above
|
||||
|
||||
return cookieStrings.map((cookieString) =>
|
||||
this.parseSetCookieString(cookieString)
|
||||
)
|
||||
})
|
||||
.filter((cookie) => {
|
||||
// Perform the required checks on the cookies
|
||||
|
||||
const passesPathCheck = url.pathname.startsWith(cookie.path ?? "/")
|
||||
|
||||
const passesExpiresCheck = !cookie.expires
|
||||
? true
|
||||
: cookie.expires.getTime() >= new Date().getTime()
|
||||
|
||||
const passesSecureCheck = !cookie.secure
|
||||
? true
|
||||
: url.protocol === "https:"
|
||||
|
||||
return passesPathCheck && passesExpiresCheck && passesSecureCheck
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,12 @@ export type Interceptor<Err extends InterceptorError = InterceptorError> = {
|
||||
*/
|
||||
name: (t: ReturnType<typeof getI18n>) => MaybeRef<string>
|
||||
|
||||
/**
|
||||
* Defines whether the interceptor has support for cookies.
|
||||
* If this field is undefined, it is assumed as not supporting cookies.
|
||||
*/
|
||||
supportsCookies?: boolean
|
||||
|
||||
/**
|
||||
* Defines what to render in the Interceptor section of the Settings page.
|
||||
* Use this space to define interceptor specific settings.
|
||||
@@ -161,6 +167,16 @@ export class InterceptorService extends Service {
|
||||
Array.from(this.interceptors.values())
|
||||
)
|
||||
|
||||
/**
|
||||
* Gives an instance to the current interceptor.
|
||||
* NOTE: Do not update from here, this is only for reading.
|
||||
*/
|
||||
public currentInterceptor = computed(() => {
|
||||
if (this.currentInterceptorID.value === null) return null
|
||||
|
||||
return this.interceptors.get(this.currentInterceptorID.value)
|
||||
})
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
|
||||
27
packages/hoppscotch-selfhost-desktop/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Sitemap
|
||||
.sitemap-gen
|
||||
7
packages/hoppscotch-selfhost-desktop/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"tauri-apps.tauri-vscode",
|
||||
"rust-lang.rust-analyzer"
|
||||
]
|
||||
}
|
||||
16
packages/hoppscotch-selfhost-desktop/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Tauri + Vue 3 + TypeScript
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
|
||||
|
||||
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
|
||||
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
||||
26
packages/hoppscotch-selfhost-desktop/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hoppscotch - Open source API development ecosystem</title>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="apple-touch-icon" href="/icon.png" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
// Shims to make swagger-parser package work
|
||||
window.global = window
|
||||
</script>
|
||||
<script type="module">
|
||||
import { Buffer } from "buffer"
|
||||
import process from "process"
|
||||
|
||||
// // Shims to make postman-collection work
|
||||
window.Buffer = Buffer
|
||||
window.process = process
|
||||
</script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
118
packages/hoppscotch-selfhost-desktop/meta.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { IHTMLTag } from "vite-plugin-html-config"
|
||||
|
||||
export const APP_INFO = {
|
||||
name: "Hoppscotch",
|
||||
shortDescription: "Open source API development ecosystem",
|
||||
description:
|
||||
"Helps you create requests faster, saving precious time on development.",
|
||||
keywords:
|
||||
"hoppscotch, hopp scotch, hoppscotch online, hoppscotch app, postwoman, postwoman chrome, postwoman online, postwoman for mac, postwoman app, postwoman for windows, postwoman google chrome, postwoman chrome app, get postwoman, postwoman web, postwoman android, postwoman app for chrome, postwoman mobile app, postwoman web app, api, request, testing, tool, rest, websocket, sse, graphql, socketio",
|
||||
app: {
|
||||
background: "#202124",
|
||||
},
|
||||
social: {
|
||||
twitter: "@hoppscotch_io",
|
||||
},
|
||||
} as const
|
||||
|
||||
export const META_TAGS = (env: Record<string, string>): IHTMLTag[] => [
|
||||
{
|
||||
name: "keywords",
|
||||
content: APP_INFO.keywords,
|
||||
},
|
||||
{
|
||||
name: "X-UA-Compatible",
|
||||
content: "IE=edge, chrome=1",
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
content: `${APP_INFO.name} • ${APP_INFO.shortDescription}`,
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
content: APP_INFO.description,
|
||||
},
|
||||
{
|
||||
name: "image",
|
||||
content: `${env.VITE_BASE_URL}/banner.png`,
|
||||
},
|
||||
// Open Graph tags
|
||||
{
|
||||
name: "og:title",
|
||||
content: `${APP_INFO.name} • ${APP_INFO.shortDescription}`,
|
||||
},
|
||||
{
|
||||
name: "og:description",
|
||||
content: APP_INFO.description,
|
||||
},
|
||||
{
|
||||
name: "og:image",
|
||||
content: `${env.VITE_BASE_URL}/banner.png`,
|
||||
},
|
||||
// Twitter tags
|
||||
{
|
||||
name: "twitter:card",
|
||||
content: "summary_large_image",
|
||||
},
|
||||
{
|
||||
name: "twitter:site",
|
||||
content: APP_INFO.social.twitter,
|
||||
},
|
||||
{
|
||||
name: "twitter:creator",
|
||||
content: APP_INFO.social.twitter,
|
||||
},
|
||||
{
|
||||
name: "twitter:title",
|
||||
content: `${APP_INFO.name} • ${APP_INFO.shortDescription}`,
|
||||
},
|
||||
{
|
||||
name: "twitter:description",
|
||||
content: APP_INFO.description,
|
||||
},
|
||||
{
|
||||
name: "twitter:image",
|
||||
content: `${env.VITE_BASE_URL}/banner.png`,
|
||||
},
|
||||
// Add to homescreen for Chrome on Android. Fallback for PWA (handled by nuxt)
|
||||
{
|
||||
name: "application-name",
|
||||
content: APP_INFO.name,
|
||||
},
|
||||
// Windows phone tile icon
|
||||
{
|
||||
name: "msapplication-TileImage",
|
||||
content: `${env.VITE_BASE_URL}/icon.png`,
|
||||
},
|
||||
{
|
||||
name: "msapplication-TileColor",
|
||||
content: APP_INFO.app.background,
|
||||
},
|
||||
{
|
||||
name: "msapplication-tap-highlight",
|
||||
content: "no",
|
||||
},
|
||||
// iOS Safari
|
||||
{
|
||||
name: "apple-mobile-web-app-title",
|
||||
content: APP_INFO.name,
|
||||
},
|
||||
{
|
||||
name: "apple-mobile-web-app-capable",
|
||||
content: "yes",
|
||||
},
|
||||
{
|
||||
name: "apple-mobile-web-app-status-bar-style",
|
||||
content: "black-translucent",
|
||||
},
|
||||
// PWA
|
||||
{
|
||||
name: "theme-color",
|
||||
content: APP_INFO.app.background,
|
||||
},
|
||||
{
|
||||
name: "mask-icon",
|
||||
content: "/icon.png",
|
||||
color: APP_INFO.app.background,
|
||||
},
|
||||
]
|
||||
65
packages/hoppscotch-selfhost-desktop/package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "@hoppscotch/selfhost-desktop",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"dioc": "workspace:^",
|
||||
"@hoppscotch/common": "workspace:^",
|
||||
"@platform/auth": "^0.1.106",
|
||||
"@tauri-apps/api": "^1.3.0",
|
||||
"@tauri-apps/cli": "^1.3.0",
|
||||
"@vueuse/core": "^10.4.1",
|
||||
"axios": "^0.21.4",
|
||||
"buffer": "^6.0.3",
|
||||
"environments.api": "link:@platform/environments/environments.api",
|
||||
"event": "link:@tauri-apps/api/event",
|
||||
"fp-ts": "^2.16.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"process": "^0.11.10",
|
||||
"rxjs": "^7.8.1",
|
||||
"shell": "link:@tauri-apps/api/shell",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"tauri": "link:@tauri-apps/api/tauri",
|
||||
"tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store#v1",
|
||||
"util": "^0.12.4",
|
||||
"vue": "^3.2.45",
|
||||
"workbox-window": "^6.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"@types/node": "^18.7.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||
"@typescript-eslint/parser": "^5.19.0",
|
||||
"@vitejs/plugin-legacy": "^2.3.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.5.1",
|
||||
"typescript": "^4.9.5",
|
||||
"unplugin-icons": "^0.14.9",
|
||||
"unplugin-vue-components": "^0.21.0",
|
||||
"vite": "^4.2.1",
|
||||
"vite-plugin-fonts": "^0.6.0",
|
||||
"vite-plugin-html-config": "^1.0.10",
|
||||
"vite-plugin-inspect": "^0.7.4",
|
||||
"vite-plugin-pages": "^0.26.0",
|
||||
"vite-plugin-pages-sitemap": "^1.4.0",
|
||||
"vite-plugin-pwa": "^0.13.1",
|
||||
"vite-plugin-static-copy": "^0.12.0",
|
||||
"vite-plugin-vue-layouts": "^0.7.0",
|
||||
"vite-plugin-windicss": "^1.8.8",
|
||||
"vue-tsc": "^1.0.11",
|
||||
"windicss": "^3.5.6"
|
||||
}
|
||||
}
|
||||
6
packages/hoppscotch-selfhost-desktop/public/tauri.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
1
packages/hoppscotch-selfhost-desktop/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
4
packages/hoppscotch-selfhost-desktop/src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
4316
packages/hoppscotch-selfhost-desktop/src-tauri/Cargo.lock
generated
Normal file
47
packages/hoppscotch-selfhost-desktop/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,47 @@
|
||||
[package]
|
||||
name = "hoppscotch-desktop"
|
||||
version = "0.0.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.4.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.4.1", features = [
|
||||
"dialog-save",
|
||||
"fs-write-file",
|
||||
"http-all",
|
||||
"os-all",
|
||||
"shell-open",
|
||||
"window-start-dragging",
|
||||
"http-multipart",
|
||||
] }
|
||||
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tauri-plugin-deep-link = { git = "https://github.com/FabianLars/tauri-plugin-deep-link", branch = "main" }
|
||||
tauri-plugin-window-state = "0.1.0"
|
||||
reqwest = "0.11.20"
|
||||
serde_json = "1.0.107"
|
||||
url = "2.4.1"
|
||||
hex_color = "2.0.0"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa = "0.25.0"
|
||||
objc = "0.2.7"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.51.1", features = [
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Foundation",
|
||||
"Win32_UI_Controls",
|
||||
] }
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
19
packages/hoppscotch-selfhost-desktop/src-tauri/Info.plist
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<!-- Obviously needs to be replaced with your app's bundle identifier -->
|
||||
<string>io.hoppscotch.desktop</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<!-- register the myapp:// and myscheme:// schemes -->
|
||||
<string>hoppscotch</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
3
packages/hoppscotch-selfhost-desktop/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
BIN
packages/hoppscotch-selfhost-desktop/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 31 KiB |
BIN
packages/hoppscotch-selfhost-desktop/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
BIN
packages/hoppscotch-selfhost-desktop/src-tauri/icons/icon.icns
Normal file
BIN
packages/hoppscotch-selfhost-desktop/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
packages/hoppscotch-selfhost-desktop/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
@@ -0,0 +1 @@
|
||||
pub mod window;
|
||||
390
packages/hoppscotch-selfhost-desktop/src-tauri/src/mac/window.rs
Normal file
@@ -0,0 +1,390 @@
|
||||
use hex_color::HexColor;
|
||||
use tauri::{App, Manager, Runtime, Window};
|
||||
|
||||
// If anything breaks on macOS, this should be the place which is broken
|
||||
// We have to override Tauri (Tao) 's built-in NSWindowDelegate implementation with a
|
||||
// custom implementation so we can emit events on full screen mode changes.
|
||||
// Our custom implementation tries to mock the Tauri implementation. So please do refer to the relevant parts
|
||||
|
||||
// Apple's NSWindowDelegate reference: https://developer.apple.com/documentation/appkit/nswindowdelegate?language=objc
|
||||
// Tao's Window Delegate Implementation: https://github.com/tauri-apps/tao/blob/dev/src/platform_impl/macos/window_delegate.rs
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum ToolbarThickness {
|
||||
Thick,
|
||||
Medium,
|
||||
Thin,
|
||||
}
|
||||
|
||||
const WINDOW_CONTROL_PAD_X: f64 = 15.0;
|
||||
const WINDOW_CONTROL_PAD_Y: f64 = 23.0;
|
||||
|
||||
pub trait WindowExt {
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_transparent_titlebar(&self);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe fn set_transparent_titlebar(id: cocoa::base::id) {
|
||||
use cocoa::appkit::NSWindow;
|
||||
|
||||
id.setTitlebarAppearsTransparent_(cocoa::base::YES);
|
||||
id.setTitleVisibility_(cocoa::appkit::NSWindowTitleVisibility::NSWindowTitleHidden);
|
||||
}
|
||||
|
||||
struct UnsafeWindowHandle(*mut std::ffi::c_void);
|
||||
unsafe impl Send for UnsafeWindowHandle {}
|
||||
unsafe impl Sync for UnsafeWindowHandle {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn update_window_theme(window: &tauri::Window, color: HexColor) {
|
||||
use cocoa::appkit::{
|
||||
NSAppearance, NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight, NSWindow,
|
||||
};
|
||||
|
||||
let brightness = (color.r as u64 + color.g as u64 + color.b as u64) / 3;
|
||||
|
||||
unsafe {
|
||||
let window_handle = UnsafeWindowHandle(window.ns_window().unwrap());
|
||||
|
||||
let _ = window.run_on_main_thread(move || {
|
||||
let handle = window_handle;
|
||||
|
||||
let selected_appearance = if brightness >= 128 {
|
||||
NSAppearance(NSAppearanceNameVibrantLight)
|
||||
} else {
|
||||
NSAppearance(NSAppearanceNameVibrantDark)
|
||||
};
|
||||
|
||||
NSWindow::setAppearance(handle.0 as cocoa::base::id, selected_appearance);
|
||||
set_window_controls_pos(
|
||||
handle.0 as cocoa::base::id,
|
||||
WINDOW_CONTROL_PAD_X,
|
||||
WINDOW_CONTROL_PAD_Y,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_window_controls_pos(window: cocoa::base::id, x: f64, y: f64) {
|
||||
use cocoa::{
|
||||
appkit::{NSView, NSWindow, NSWindowButton},
|
||||
foundation::NSRect,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let close = window.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
|
||||
let miniaturize = window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
|
||||
let zoom = window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
|
||||
|
||||
let title_bar_container_view = close.superview().superview();
|
||||
|
||||
let close_rect: NSRect = msg_send![close, frame];
|
||||
let button_height = close_rect.size.height;
|
||||
|
||||
let title_bar_frame_height = button_height + y;
|
||||
let mut title_bar_rect = NSView::frame(title_bar_container_view);
|
||||
title_bar_rect.size.height = title_bar_frame_height;
|
||||
title_bar_rect.origin.y = NSView::frame(window).size.height - title_bar_frame_height;
|
||||
let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect];
|
||||
|
||||
let window_buttons = vec![close, miniaturize, zoom];
|
||||
let space_between = NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;
|
||||
|
||||
for (i, button) in window_buttons.into_iter().enumerate() {
|
||||
let mut rect: NSRect = NSView::frame(button);
|
||||
rect.origin.x = x + (i as f64 * space_between);
|
||||
button.setFrameOrigin(rect.origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> WindowExt for Window<R> {
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_transparent_titlebar(&self) {
|
||||
unsafe {
|
||||
let id = self.ns_window().unwrap() as cocoa::base::id;
|
||||
|
||||
set_transparent_titlebar(id);
|
||||
|
||||
set_window_controls_pos(id, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Debug)]
|
||||
struct HoppAppState {
|
||||
window: Window,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn setup_mac_window(app: &mut App) {
|
||||
use cocoa::appkit::NSWindow;
|
||||
use cocoa::base::{id, BOOL};
|
||||
use cocoa::foundation::NSUInteger;
|
||||
use objc::runtime::{Object, Sel};
|
||||
use std::ffi::c_void;
|
||||
|
||||
fn with_hopp_app<F: FnOnce(&mut HoppAppState) -> T, T>(this: &Object, func: F) {
|
||||
let ptr = unsafe {
|
||||
let x: *mut c_void = *this.get_ivar("hoppApp");
|
||||
&mut *(x as *mut HoppAppState)
|
||||
};
|
||||
func(ptr);
|
||||
}
|
||||
|
||||
let window = app.get_window("main").unwrap();
|
||||
|
||||
unsafe {
|
||||
let ns_win = window.ns_window().unwrap() as id;
|
||||
|
||||
let current_delegate: id = ns_win.delegate();
|
||||
|
||||
extern "C" fn on_window_should_close(this: &Object, _cmd: Sel, sender: id) -> BOOL {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, windowShouldClose: sender]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_will_close(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowWillClose: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_resize(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
with_hopp_app(&*this, |state| {
|
||||
let id = state.window.ns_window().unwrap() as id;
|
||||
|
||||
set_window_controls_pos(id, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y);
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidResize: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_move(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidMove: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_change_backing_properties(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidChangeBackingProperties: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_become_key(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidBecomeKey: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_resign_key(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidResignKey: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_dragging_entered(this: &Object, _cmd: Sel, notification: id) -> BOOL {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, draggingEntered: notification]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_prepare_for_drag_operation(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) -> BOOL {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, prepareForDragOperation: notification]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_perform_drag_operation(this: &Object, _cmd: Sel, sender: id) -> BOOL {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, performDragOperation: sender]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_conclude_drag_operation(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, concludeDragOperation: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_dragging_exited(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, draggingExited: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_will_use_full_screen_presentation_options(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
window: id,
|
||||
proposed_options: NSUInteger,
|
||||
) -> NSUInteger {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, window: window willUseFullScreenPresentationOptions: proposed_options]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_enter_full_screen(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
with_hopp_app(&*this, |state| {
|
||||
state.window.emit("did-enter-fullscreen", ()).unwrap();
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidEnterFullScreen: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_will_enter_full_screen(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
with_hopp_app(&*this, |state| {
|
||||
state.window.emit("will-enter-fullscreen", ()).unwrap();
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowWillEnterFullScreen: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_exit_full_screen(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
with_hopp_app(&*this, |state| {
|
||||
state.window.emit("did-exit-fullscreen", ()).unwrap();
|
||||
|
||||
let id = state.window.ns_window().unwrap() as id;
|
||||
set_window_controls_pos(id, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y);
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidExitFullScreen: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_will_exit_full_screen(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
with_hopp_app(&*this, |state| {
|
||||
state.window.emit("will-exit-fullscreen", ()).unwrap();
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowWillExitFullScreen: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_fail_to_enter_full_screen(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
window: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidFailToEnterFullScreen: window];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_effective_appearance_did_change(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, effectiveAppearanceDidChange: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_effective_appearance_did_changed_on_main_thread(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![
|
||||
super_del,
|
||||
effectiveAppearanceDidChangedOnMainThread: notification
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// extern fn on_dealloc(this: &Object, cmd: Sel) {
|
||||
// unsafe {
|
||||
// let super_del: id = *this.get_ivar("super_delegate");
|
||||
// let _: () = msg_send![super_del, dealloc];
|
||||
// }
|
||||
// }
|
||||
|
||||
// extern fn on_mark_is_checking_zoomed_in(this: &Object, cmd: Sel) {
|
||||
// unsafe {
|
||||
// let super_del: id = *this.get_ivar("super_delegate");
|
||||
// let _: () = msg_send![super_del, markIsCheckingZoomedIn];
|
||||
// }
|
||||
// }
|
||||
|
||||
// extern fn on_clear_is_checking_zoomed_in(this: &Object, cmd: Sel) {
|
||||
// unsafe {
|
||||
// let super_del: id = *this.get_ivar("super_delegate");
|
||||
// let _: () = msg_send![super_del, clearIsCheckingZoomedIn];
|
||||
// }
|
||||
// }
|
||||
|
||||
// Are we deallocing this properly ? (I miss safe Rust :( )
|
||||
let app_state = HoppAppState { window };
|
||||
let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void;
|
||||
|
||||
ns_win.setDelegate_(delegate!("MainWindowDelegate", {
|
||||
window: id = ns_win,
|
||||
hoppApp: *mut c_void = app_box,
|
||||
toolbar: id = cocoa::base::nil,
|
||||
super_delegate: id = current_delegate,
|
||||
// (dealloc) => on_dealloc as extern fn(&Object, Sel),
|
||||
// (markIsCheckingZoomedIn) => on_mark_is_checking_zoomed_in as extern fn(&Object, Sel),
|
||||
// (clearIsCheckingZoomedIn) => on_clear_is_checking_zoomed_in as extern fn(&Object, Sel),
|
||||
(windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id),
|
||||
(windowDidResize:) => on_window_did_resize as extern fn(&Object, Sel, id),
|
||||
(windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id),
|
||||
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id),
|
||||
(windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id),
|
||||
(windowDidResignKey:) => on_window_did_resign_key as extern fn(&Object, Sel, id),
|
||||
(draggingEntered:) => on_dragging_entered as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(performDragOperation:) => on_perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id),
|
||||
(draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id),
|
||||
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
|
||||
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen as extern fn(&Object, Sel, id),
|
||||
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen as extern fn(&Object, Sel, id),
|
||||
(windowDidExitFullScreen:) => on_window_did_exit_full_screen as extern fn(&Object, Sel, id),
|
||||
(windowWillExitFullScreen:) => on_window_will_exit_full_screen as extern fn(&Object, Sel, id),
|
||||
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id),
|
||||
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id),
|
||||
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id)
|
||||
}))
|
||||
}
|
||||
|
||||
app.get_window("main").unwrap().set_transparent_titlebar();
|
||||
|
||||
let window_handle = app.get_window("main").unwrap();
|
||||
update_window_theme(&window_handle, HexColor::WHITE);
|
||||
|
||||
// Control window theme based on app update_window
|
||||
app.listen_global("hopp-bg-changed", move |ev| {
|
||||
let payload = serde_json::from_str::<&str>(ev.payload().unwrap())
|
||||
.unwrap()
|
||||
.trim();
|
||||
|
||||
let color = HexColor::parse_rgb(payload).unwrap();
|
||||
|
||||
update_window_theme(&window_handle, color);
|
||||
});
|
||||
}
|
||||
52
packages/hoppscotch-selfhost-desktop/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[macro_use]
|
||||
extern crate cocoa;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win;
|
||||
|
||||
use tauri::Manager;
|
||||
|
||||
fn main() {
|
||||
tauri_plugin_deep_link::prepare("io.hoppscotch.desktop");
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.setup(|app| {
|
||||
if cfg!(target_os = "macos") {
|
||||
#[cfg(target_os = "macos")]
|
||||
use mac::window::setup_mac_window;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
setup_mac_window(app);
|
||||
} else if cfg!(target_os = "windows") {
|
||||
#[cfg(target_os = "windows")]
|
||||
use win::window::setup_win_window;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
setup_win_window(app);
|
||||
}
|
||||
|
||||
let handle = app.handle();
|
||||
tauri_plugin_deep_link::register("hoppscotch", move |request| {
|
||||
println!("{:?}", request);
|
||||
handle.emit_all("scheme-request-received", request).unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod window;
|
||||
@@ -0,0 +1,86 @@
|
||||
use hex_color::HexColor;
|
||||
use tauri::App;
|
||||
use tauri::Manager;
|
||||
|
||||
use std::mem::transmute;
|
||||
use std::{ptr, ffi::c_void, mem::size_of};
|
||||
|
||||
use windows::Win32::UI::Controls::{WTA_NONCLIENT, WTNCA_NODRAWICON, WTNCA_NOSYSMENU, WTNCA_NOMIRRORHELP};
|
||||
|
||||
use windows::Win32::UI::Controls::SetWindowThemeAttribute;
|
||||
use windows::Win32::UI::Controls::WTNCA_NODRAWCAPTION;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_CAPTION_COLOR;
|
||||
use windows::Win32::Foundation::COLORREF;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Graphics::Dwm::{DWMWA_USE_IMMERSIVE_DARK_MODE};
|
||||
|
||||
fn hex_color_to_colorref(color: HexColor) -> COLORREF {
|
||||
// TODO: Remove this unsafe, This operation doesn't need to be unsafe!
|
||||
unsafe {
|
||||
COLORREF(transmute::<[u8; 4], u32>([color.r, color.g, color.b, 0]))
|
||||
}
|
||||
}
|
||||
|
||||
struct WinThemeAttribute {
|
||||
flag: u32,
|
||||
mask: u32
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn update_bg_color(hwnd: &HWND, bg_color: HexColor) {
|
||||
|
||||
let use_dark_mode = BOOL::from(true);
|
||||
|
||||
let final_color = hex_color_to_colorref(bg_color);
|
||||
|
||||
unsafe {
|
||||
DwmSetWindowAttribute(
|
||||
HWND(hwnd.0),
|
||||
DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
ptr::addr_of!(use_dark_mode) as *const c_void,
|
||||
size_of::<BOOL>().try_into().unwrap()
|
||||
).unwrap();
|
||||
|
||||
DwmSetWindowAttribute(
|
||||
HWND(hwnd.0),
|
||||
DWMWA_CAPTION_COLOR,
|
||||
ptr::addr_of!(final_color) as *const c_void,
|
||||
size_of::<COLORREF>().try_into().unwrap()
|
||||
).unwrap();
|
||||
|
||||
let flags = WTNCA_NODRAWCAPTION | WTNCA_NODRAWICON;
|
||||
let mask = WTNCA_NODRAWCAPTION | WTNCA_NODRAWICON | WTNCA_NOSYSMENU | WTNCA_NOMIRRORHELP;
|
||||
let options = WinThemeAttribute { flag: flags, mask };
|
||||
|
||||
SetWindowThemeAttribute(
|
||||
HWND(hwnd.0),
|
||||
WTA_NONCLIENT,
|
||||
ptr::addr_of!(options) as *const c_void,
|
||||
size_of::<WinThemeAttribute>().try_into().unwrap()
|
||||
).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn setup_win_window(app: &mut App) {
|
||||
|
||||
let window = app.get_window("main").unwrap();
|
||||
let win_handle = window.hwnd().unwrap();
|
||||
|
||||
let win_clone = win_handle.clone();
|
||||
|
||||
app.listen_global("hopp-bg-changed", move |ev| {
|
||||
|
||||
let payload = serde_json::from_str::<&str>(ev.payload().unwrap())
|
||||
.unwrap()
|
||||
.trim();
|
||||
|
||||
let color = HexColor::parse_rgb(payload).unwrap();
|
||||
|
||||
update_bg_color(&HWND(win_clone.0), color);
|
||||
});
|
||||
|
||||
update_bg_color(&HWND(win_handle.0), HexColor::rgb(23, 23, 23));
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"devPath": "http://localhost:3000",
|
||||
"distDir": "../dist",
|
||||
"withGlobalTauri": false
|
||||
},
|
||||
"package": {
|
||||
"productName": "hoppscotch-desktop",
|
||||
"version": "0.0.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true
|
||||
},
|
||||
"os": {
|
||||
"all": true
|
||||
},
|
||||
"fs": {
|
||||
"writeFile": true
|
||||
},
|
||||
"dialog": {
|
||||
"save": true
|
||||
},
|
||||
"http": {
|
||||
"all": true,
|
||||
"request": true,
|
||||
"scope": ["http://*", "https://*", "wss://*"]
|
||||
},
|
||||
"window": {
|
||||
"startDragging": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"identifier": "io.hoppscotch.desktop",
|
||||
"targets": "all"
|
||||
},
|
||||
"security": {
|
||||
"csp": "none"
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"resizable": true,
|
||||
"title": "Hoppscotch",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
1857
packages/hoppscotch-selfhost-desktop/src/api/generated/graphql.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
mutation ClearGlobalEnvironments($id: ID!) {
|
||||
clearGlobalEnvironments(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mutation CreateGQLChildUserCollection(
|
||||
$title: String!
|
||||
$parentUserCollectionID: ID!
|
||||
) {
|
||||
createGQLChildUserCollection(
|
||||
title: $title
|
||||
parentUserCollectionID: $parentUserCollectionID
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation CreateGQLRootUserCollection($title: String!) {
|
||||
createGQLRootUserCollection(title: $title) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
mutation CreateGQLUserRequest(
|
||||
$title: String!
|
||||
$request: String!
|
||||
$collectionID: ID!
|
||||
) {
|
||||
createGQLUserRequest(
|
||||
title: $title
|
||||
request: $request
|
||||
collectionID: $collectionID
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mutation CreateRESTChildUserCollection(
|
||||
$title: String!
|
||||
$parentUserCollectionID: ID!
|
||||
) {
|
||||
createRESTChildUserCollection(
|
||||
title: $title
|
||||
parentUserCollectionID: $parentUserCollectionID
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation CreateRESTRootUserCollection($title: String!) {
|
||||
createRESTRootUserCollection(title: $title) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
mutation CreateRESTUserRequest(
|
||||
$collectionID: ID!
|
||||
$title: String!
|
||||
$request: String!
|
||||
) {
|
||||
createRESTUserRequest(
|
||||
collectionID: $collectionID
|
||||
title: $title
|
||||
request: $request
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
mutation CreateUserEnvironment($name: String!, $variables: String!) {
|
||||
createUserEnvironment(name: $name, variables: $variables) {
|
||||
id
|
||||
userUid
|
||||
name
|
||||
variables
|
||||
isGlobal
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation CreateUserGlobalEnvironment($variables: String!) {
|
||||
createUserGlobalEnvironment(variables: $variables) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
mutation CreateUserHistory(
|
||||
$reqData: String!
|
||||
$resMetadata: String!
|
||||
$reqType: ReqType!
|
||||
) {
|
||||
createUserHistory(
|
||||
reqData: $reqData
|
||||
resMetadata: $resMetadata
|
||||
reqType: $reqType
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation CreateUserSettings($properties: String!) {
|
||||
createUserSettings(properties: $properties) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mutation DeleteAllUserHistory($reqType: ReqType!) {
|
||||
deleteAllUserHistory(reqType: $reqType) {
|
||||
count
|
||||
reqType
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation DeleteUserCollection($userCollectionID: ID!) {
|
||||
deleteUserCollection(userCollectionID: $userCollectionID)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation DeleteUserEnvironment($id: ID!) {
|
||||
deleteUserEnvironment(id: $id)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation DeleteUserRequest($requestID: ID!) {
|
||||
deleteUserRequest(id: $requestID)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
mutation MoveUserCollection($destCollectionID: ID, $userCollectionID: ID!) {
|
||||
moveUserCollection(
|
||||
destCollectionID: $destCollectionID
|
||||
userCollectionID: $userCollectionID
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
mutation MoveUserRequest(
|
||||
$sourceCollectionID: ID!
|
||||
$requestID: ID!
|
||||
$destinationCollectionID: ID!
|
||||
$nextRequestID: ID
|
||||
) {
|
||||
moveUserRequest(
|
||||
sourceCollectionID: $sourceCollectionID
|
||||
requestID: $requestID
|
||||
destinationCollectionID: $destinationCollectionID
|
||||
nextRequestID: $nextRequestID
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation RemoveRequestFromHistory($id: ID!) {
|
||||
removeRequestFromHistory(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
mutation RenameUserCollection($userCollectionID: ID!, $newTitle: String!) {
|
||||
renameUserCollection(
|
||||
userCollectionID: $userCollectionID
|
||||
newTitle: $newTitle
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation ToggleHistoryStarStatus($id: ID!) {
|
||||
toggleHistoryStarStatus(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation UpdateGQLUserRequest($id: ID!, $request: String!, $title: String) {
|
||||
updateGQLUserRequest(id: $id, request: $request, title: $title) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mutation UpdateRESTUserRequest($id: ID!, $title: String!, $request: String!) {
|
||||
updateRESTUserRequest(id: $id, title: $title, request: $request) {
|
||||
id
|
||||
collectionID
|
||||
request
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mutation UpdateUserCollectionOrder($collectionID: ID!, $nextCollectionID: ID) {
|
||||
updateUserCollectionOrder(
|
||||
collectionID: $collectionID
|
||||
nextCollectionID: $nextCollectionID
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
mutation UpdateUserEnvironment($id: ID!, $name: String!, $variables: String!) {
|
||||
updateUserEnvironment(id: $id, name: $name, variables: $variables) {
|
||||
id
|
||||
userUid
|
||||
name
|
||||
variables
|
||||
isGlobal
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mutation UpdateUserSession(
|
||||
$currentSession: String!
|
||||
$sessionType: SessionType!
|
||||
) {
|
||||
updateUserSessions(
|
||||
currentSession: $currentSession
|
||||
sessionType: $sessionType
|
||||
) {
|
||||
currentRESTSession
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation UpdateUserSettings($properties: String!) {
|
||||
updateUserSettings(properties: $properties) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
mutation CreateUserEnvironment($name: String!, $variables: String!) {
|
||||
createUserEnvironment(name: $name, variables: $variables) {
|
||||
id
|
||||
userUid
|
||||
name
|
||||
variables
|
||||
isGlobal
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
query ExportUserCollectionsToJSON(
|
||||
$collectionID: ID
|
||||
$collectionType: ReqType!
|
||||
) {
|
||||
exportUserCollectionsToJSON(
|
||||
collectionID: $collectionID
|
||||
collectionType: $collectionType
|
||||
) {
|
||||
collectionType
|
||||
exportedCollection
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
query GetCurrentRESTSession {
|
||||
me {
|
||||
currentRESTSession
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
query GetGlobalEnvironments {
|
||||
me {
|
||||
globalEnvironments {
|
||||
id
|
||||
isGlobal
|
||||
name
|
||||
userUid
|
||||
variables
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
query GetRESTUserHistory {
|
||||
me {
|
||||
RESTHistory {
|
||||
id
|
||||
userUid
|
||||
reqType
|
||||
request
|
||||
responseMetadata
|
||||
isStarred
|
||||
executedOn
|
||||
}
|
||||
|
||||
GQLHistory {
|
||||
id
|
||||
userUid
|
||||
reqType
|
||||
request
|
||||
responseMetadata
|
||||
isStarred
|
||||
executedOn
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
query GetGQLRootUserCollections {
|
||||
# the frontend doesnt paginate right now, so giving take a big enough value to get all collections at once
|
||||
rootGQLUserCollections(take: 99999) {
|
||||
id
|
||||
title
|
||||
type
|
||||
childrenGQL {
|
||||
id
|
||||
title
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
query GetUserEnvironments {
|
||||
me {
|
||||
environments {
|
||||
id
|
||||
isGlobal
|
||||
name
|
||||
userUid
|
||||
variables
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
query GetUserRootCollections {
|
||||
# the frontend doesnt paginate right now, so giving take a big enough value to get all collections at once
|
||||
rootRESTUserCollections(take: 99999) {
|
||||
id
|
||||
title
|
||||
type
|
||||
childrenREST {
|
||||
id
|
||||
title
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
query GetUserSettings {
|
||||
me {
|
||||
settings {
|
||||
id
|
||||
properties
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
subscription UserCollectionCreated {
|
||||
userCollectionCreated {
|
||||
parent {
|
||||
id
|
||||
}
|
||||
id
|
||||
title
|
||||
type
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
subscription UserCollectionMoved {
|
||||
userCollectionMoved {
|
||||
id
|
||||
parent {
|
||||
id
|
||||
}
|
||||
type
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
subscription UserCollectionOrderUpdated {
|
||||
userCollectionOrderUpdated {
|
||||
userCollection {
|
||||
id
|
||||
parent {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
nextUserCollection {
|
||||
id
|
||||
parent {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
subscription UserCollectionRemoved {
|
||||
userCollectionRemoved {
|
||||
id
|
||||
type
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
subscription userCollectionUpdated {
|
||||
userCollectionUpdated {
|
||||
id
|
||||
title
|
||||
type
|
||||
parent {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
subscription UserEnvironmentCreated {
|
||||
userEnvironmentCreated {
|
||||
id
|
||||
isGlobal
|
||||
name
|
||||
userUid
|
||||
variables
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
subscription UserEnvironmentDeleted {
|
||||
userEnvironmentDeleted {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
subscription UserEnvironmentUpdated {
|
||||
userEnvironmentUpdated {
|
||||
id
|
||||
userUid
|
||||
name
|
||||
variables
|
||||
isGlobal
|
||||
}
|
||||
}
|
||||