Commit 1ee1ea77 authored by Domi's avatar Domi

feat: sidebar multitab

parent f1552320
......@@ -18,8 +18,10 @@
"lodash-es": "^4.17.21",
"mammoth": "^1.6.0",
"pdfjs-dist": "^4.0.269",
"punycode.js": "^2.3.1",
"tiktoken": "^1.0.11",
"turndown": "^7.1.2",
"ua-parser-js": "^1.0.37",
"vite-plugin-top-level-await": "^1.4.1",
"vite-plugin-wasm": "^3.3.0",
"vue": "^3.3.4",
......@@ -8752,6 +8754,14 @@
"pump": "^2.0.0"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"engines": {
"node": ">=6"
}
},
"node_modules/q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
......@@ -10273,6 +10283,28 @@
"node": ">=14.17"
}
},
"node_modules/ua-parser-js": {
"version": "1.0.37",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz",
"integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"engines": {
"node": "*"
}
},
"node_modules/uberproto": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/uberproto/-/uberproto-1.2.0.tgz",
......
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-7-.5-14.5T799-507q-5 29-27 48t-52 19h-80q-33 0-56.5-23.5T560-520v-40H400v-80q0-33 23.5-56.5T480-720h40q0-23 12.5-40.5T563-789q-20-5-40.5-8t-42.5-3q-134 0-227 93t-93 227h200q66 0 113 47t47 113v40H400v110q20 5 39.5 7.5T480-160Z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="#888"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-7-.5-14.5T799-507q-5 29-27 48t-52 19h-80q-33 0-56.5-23.5T560-520v-40H400v-80q0-33 23.5-56.5T480-720h40q0-23 12.5-40.5T563-789q-20-5-40.5-8t-42.5-3q-134 0-227 93t-93 227h200q66 0 113 47t47 113v40H400v110q20 5 39.5 7.5T480-160Z"/></svg>
\ No newline at end of file
{
"data": {
"configVersion": 20240310,
"configVersion": 20240330,
"chatDocSites": [
{
"host": "huggingface.co",
......@@ -135,6 +135,13 @@
],
"search": {
"defaultEngine": "google"
},
"embedView": {
"defaultUA": "",
"hostUA": {
"www.google.com": 1,
"bing.com": 1
}
}
}
}
......@@ -6,13 +6,13 @@
font-weight: normal;
}
a,
/* a,
.green {
text-decoration: none;
/* color: hsla(160, 100%, 37%, 1); */
color: hsla(160, 100%, 37%, 1);
color: #38bdf8;
transition: 0.4s;
}
} */
@media (hover: hover) {
a:hover {
......
......@@ -164,7 +164,7 @@ function handleMessage(message: any, sender: chrome.runtime.MessageSender) {
case MessageType.setupOffscreenDocument:
return offscreen.setup()
case MessageType.fromOffscreen:
return offscreen.handleMessage(message)
return offscreen.handleResMsg(message)
case MessageType.invokeRequest:
handleInvokeRequest(message, sender)
break
......
import { MessageType, ServiceFunc, type ParseDocOptions } from "@/types"
import Invoke from "@/utils/Invoke"
import { Invoke } from "@/utils/invoke"
let creating: Promise<void> | null // A global promise to avoid concurrency issues
......@@ -60,7 +60,7 @@ class Offscreen extends Invoke {
return { key, response }
}
public handleMessage(message: any): void {
public handleResMsg(message: any): void {
const { type, key, payload, success } = message
if (type === MessageType.fromOffscreen) {
this.setReturnValue(key, success, payload)
......
......@@ -19,7 +19,7 @@ export async function handleContentMounted(tabId: number) {
const sidebar = contentSidebarMap.get(tabId)
if (sidebar) {
await chrome.storage.session.set({
sidebarUrls: { content: sidebar.url },
sidebarInitUrl: { content: sidebar.url },
})
chrome.tabs.sendMessage(tabId, {
type: MessageType.openContentSidebar,
......
......@@ -32,6 +32,7 @@ const engine = ref(Engine.google)
const engineListVisible = ref(false)
let timerId = -1
let timerId2 = -1
watch(focus, (value) => {
if (value) {
......@@ -103,20 +104,37 @@ function switchEngine(value: Engine) {
engine.value = value
chrome.storage.local.set({ searchEngine: value })
}
function handlePointerLeave() {
timerId2 = window.setTimeout(() => {
engineListVisible.value = false
}, 800)
}
function handlePointerEnter() {
clearTimeout(timerId2)
}
</script>
<template>
<div class="relative h-10 w-full">
<div
class="absolute border border-foreground/20 rounded-[20px] w-full shadow bg-background z-10 overflow-hidden"
:class="[
'absolute border border-foreground/20 rounded-[20px] w-full shadow bg-background z-10 overflow-hidden',
'focus-within:shadow-md',
]"
@pointerleave="handlePointerLeave"
@pointerenter="handlePointerEnter"
>
<div aria-label="search box" class="flex px-2 items-center">
<button
class="size-8 flex items-center justify-center shrink-0 rounded-full hover:bg-background-soft cursor-pointer"
:class="[
'group size-8 flex items-center justify-center shrink-0 rounded-full hover:bg-background-soft cursor-pointer',
]"
@click="engineListVisible = !engineListVisible"
>
<img
class="size-5"
class="size-5 group-active:scale-90 transition-transform"
:src="engine == Engine.bing ? bingImg : googleImg"
/>
</button>
......
......@@ -11,29 +11,32 @@ defineProps<{
title: string
badge?: "popup" | "sidebar" | "remove"
small?: boolean
url?: string
}>()
defineEmits(["click", "remove"])
</script>
<template>
<button
<a
:class="[
'group shrink-0 relative flex flex-col items-center justify-self-center rounded-lg py-1 px-0.5 hover:bg-background-soft',
'group shrink-0 relative flex flex-col items-center justify-self-center rounded-lg ',
'py-1 px-0.5 hover:bg-background-soft text-inherit cursor-pointer',
small ? 'w-[58px]' : 'w-16',
]"
@click="$emit('click')"
@click="(e) => (e.preventDefault(), $emit('click'))"
:href="url"
>
<div class="p-2.5 rounded-full bg-background-soft">
<img
class="size-6 rounded"
class="size-6 rounded pointer-events-none"
:src="icon"
:data-fallback="globeImg"
loading="lazy"
@error="handleImgError"
/>
</div>
<div class="flex flex-col justify-center h-8 w-full">
<div class="flex flex-col justify-center h-8 w-full text-center">
<div class="text-xs max-w-full break-words leading-tight line-clamp-2">
{{ title }}
</div>
......@@ -64,7 +67,7 @@ defineEmits(["click", "remove"])
>
<IconClose class="size-3" />
</div>
</button>
</a>
</template>
<style scoped></style>
<script setup lang="ts">
import { ref, reactive, watch, onMounted, computed, onUnmounted } from "vue"
import config from "@/assets/config.json"
import { getLocal, updateFrameNetRules } from "@/utils/ext"
import { ContentScriptId, FrameMessageType } from "@/types"
import { updateFrameNetRules } from "@/utils/ext"
import {
ContentScriptId,
FrameMessageType,
WindowName,
WebviewFunc,
} from "@/types"
import { findFrameLoadUrl } from "@/utils/utils"
import { fetchDoc } from "@/content/pip"
import { WebviewInvoke } from "@/utils/invoke"
export type PageInfo = {
url: string
title: string
icon: string
}
const props = defineProps<{
url: string
ua?: string
preloadUrl?: string
preloadCandidates?: string[]
}>()
const emit = defineEmits<{
pageInfo: [pageInfo: { url: string; title: string; icon: string }]
load: [PageInfo]
}>()
defineExpose({
......@@ -24,59 +37,42 @@ defineExpose({
const onceCallback = new Map<string, (e: MessageEvent) => boolean>()
const frame = ref<HTMLIFrameElement>()
const patchs = reactive(config.data.webviewPatchs)
const loadUrls = reactive(config.data.loadCandidates)
const inited = ref(false)
const frameUrl = ref("")
const pageInfo = reactive({ url: "", title: "", icon: "" })
const webviewInvoke = ref<WebviewInvoke>()
watch(
() => props.ua,
async (ua) => {
const tab = await chrome.tabs.getCurrent()
await updateFrameNetRules({
ua: ua,
tabIds: [tab?.id || -1],
initiatorDomains: [chrome.runtime.id],
})
}
)
onMounted(() => {
getLocal({
webviewPatchs: config.data.webviewPatchs,
loadCandidates: config.data.loadCandidates,
}).then(({ webviewPatchs, loadCandidates }) => {
if (webviewPatchs) {
patchs.splice(0, patchs.length, ...webviewPatchs)
}
if (loadCandidates) {
loadUrls.splice(0, loadUrls.length, ...loadCandidates)
}
inited.value = true
})
webviewInvoke.value = new WebviewInvoke("webview", frame.value!)
window.addEventListener("message", handleFrameMessage)
console.log("loadFrame", props.url, props.ua)
loadFrame(props.url, props.ua)
})
onUnmounted(() => {
window.removeEventListener("message", handleFrameMessage)
})
const patch = computed(() => {
const url = inited.value ? props.url : ""
const patch = patchs.find((p) => {
try {
return new RegExp(p.re).test(url)
} catch (e) {
console.error(e)
}
return false
})
return {
url,
ua: patch?.ua || props.ua || "",
l: patch?.l || "",
}
})
watch(patch, async (patch) => {
async function loadFrame(url: string, ua?: string) {
const iframe = frame.value
if (!patch.url || !iframe) return
if (!url || !iframe) return
const tab = await chrome.tabs.getCurrent()
await updateFrameNetRules({
ua: patch.ua,
ua: ua,
tabIds: [tab?.id || -1],
initiatorDomains: [chrome.runtime.id],
})
await chrome.scripting.updateContentScripts([
......@@ -90,7 +86,7 @@ watch(patch, async (patch) => {
},
])
frameUrl.value = patch.url
frameUrl.value = url
pageInfo.url = ""
pageInfo.title = ""
pageInfo.icon = ""
......@@ -108,12 +104,19 @@ watch(patch, async (patch) => {
})
} catch (e) {
console.warn(e)
const url = new URL(props.url)
let loadUrl = url.origin + patch.l
if (!loadUrl) {
const u = await findFrameLoadUrl(loadUrls)
let loadUrl = ""
if (props.preloadUrl) {
const u = new URL(props.preloadUrl || "", props.url)
loadUrl = u.href
}
if (!loadUrl && props.preloadCandidates) {
const u = await findFrameLoadUrl(props.preloadCandidates, props.url)
u && (loadUrl = u)
}
if (!loadUrl) {
console.warn("No preload url found")
return
}
try {
frameUrl.value = loadUrl
......@@ -148,7 +151,7 @@ watch(patch, async (patch) => {
},
"*"
)
})
}
function handleFrameMessage(e: MessageEvent) {
console.log("frame message: ", e, e.source !== frame.value?.contentWindow)
......@@ -164,47 +167,56 @@ function handleFrameMessage(e: MessageEvent) {
pageInfo.title = e.data.title
pageInfo.icon = e.data.icon
emit("pageInfo", pageInfo)
emit("load", pageInfo)
}
break
case FrameMessageType.invokeResponse:
webviewInvoke.value?.handleResMsg(e.data)
break
}
}
function reload() {
console.log("reload")
frame.value?.contentWindow?.postMessage(
{
type: FrameMessageType.reload,
},
"*"
)
console.log("reload", pageInfo.url)
webviewInvoke.value
?.invoke({
func: WebviewFunc.reload,
args: [],
timeout: 1000,
})
.catch((e) => {
console.warn("reload failed", e)
if (frame.value) {
frame.value.src = pageInfo.url || props.url
}
})
}
function goBack() {
console.log("goBack")
frame.value?.contentWindow?.postMessage(
{
type: FrameMessageType.goBack,
},
"*"
)
webviewInvoke.value?.invoke({
func: WebviewFunc.goBack,
args: [],
})
}
function goForward() {
console.log("goForward")
frame.value?.contentWindow?.postMessage(
{
type: FrameMessageType.goForward,
},
"*"
)
webviewInvoke.value?.invoke({
func: WebviewFunc.goForward,
args: [],
})
}
</script>
<template>
<iframe class="w-full h-full bg-white" ref="frame" :src="frameUrl"></iframe>
<iframe
class="w-full h-full bg-white"
ref="frame"
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
:src="frameUrl"
:name="WindowName.webview"
></iframe>
</template>
<style scoped></style>
......@@ -9,7 +9,7 @@ import { useI18n } from "@/utils/i18n"
import { getDocItem, devConfig, type SiteConfig } from "./helper"
import config from "@/assets/config.json"
import { getLocal } from "@/utils/ext"
import { chatDocPrompt } from "@/utils/prompt"
import { chatDocPrompt } from "@/utils/const"
import { autoPointerCapture } from "@/utils/dom"
const { t } = useI18n()
......@@ -211,4 +211,4 @@ onUnmounted(() => {
</div>
</template>
<style scoped></style>
\ No newline at end of file
<style scoped></style>@/utils/const
\ No newline at end of file
<script setup lang="ts">
import { watch, computed, reactive, ref, onMounted, watchEffect } from "vue"
import { chatDocsPanel } from "@/store/content"
import { contentService } from "@/utils/service"
import { contentInvoke } from "@/utils/invoke"
import { convertBlobToBase64, semanticClip } from "@/utils/utils"
import ScrollView from "@/components/ScrollView.vue"
import IconArrowBack from "@/components/icons/IconArrowBack.vue"
......@@ -21,7 +21,7 @@ import {
getInputValue,
} from "@/utils/dom"
import { getLocal } from "@/utils/ext"
import { chatDocPrompt } from "@/utils/prompt"
import { chatDocPrompt } from "@/utils/const"
import { useI18n } from "@/utils/i18n"
type Sheet = "" | "docSelect" | "promptTemplate"
......@@ -126,7 +126,7 @@ watch(
if (maxInputType !== "token") return
if (!message) return
const tokenLength = await contentService.calcTokens(message)
const tokenLength = await contentInvoke.calcTokens(message)
const rate = (message.length / tokenLength) * 0.95
const exceedMaxInput = tokenLength > maxInput
if (exceedMaxInput) {
......@@ -172,7 +172,7 @@ watch(
if (item.kind == "file" && typeof item.data != "string") {
const url = await convertBlobToBase64(item.data)
const results = await contentService.parseDoc({
const results = await contentInvoke.parseDoc({
key: item.key,
type: item.type,
size: item.data.size,
......@@ -640,4 +640,4 @@ input:hover {
transform: translate(100%, 0);
}
}
</style>
\ No newline at end of file
</style>@/utils/const@/utils/invoke/service@/utils/invokeb
\ No newline at end of file
<template>
<svg
fill="currentColor"
stroke-width="0"
viewBox="0 0 24 24"
aria-hidden="true"
height="200px"
width="200px"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10.5 18.75a.75.75 0 000 1.5h3a.75.75 0 000-1.5h-3z"></path>
<path
fill-rule="evenodd"
d="M8.625.75A3.375 3.375 0 005.25 4.125v15.75a3.375 3.375 0 003.375 3.375h6.75a3.375 3.375 0 003.375-3.375V4.125A3.375 3.375 0 0015.375.75h-6.75zM7.5 4.125C7.5 3.504 8.004 3 8.625 3H9.75v.375c0 .621.504 1.125 1.125 1.125h2.25c.621 0 1.125-.504 1.125-1.125V3h1.125c.621 0 1.125.504 1.125 1.125v15.75c0 .621-.504 1.125-1.125 1.125h-6.75A1.125 1.125 0 017.5 19.875V4.125z"
clip-rule="evenodd"
></path>
</svg>
</template>
......@@ -5,7 +5,7 @@ import { getLocal } from "@/utils/ext"
import IconClose from "@/components/icons/IconClose.vue"
import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue"
import PageScrollbar from "./PageScrollbar.vue"
import { MessageType } from "@/types"
import { FrameMessageType, MessageType } from "@/types"
import { autoPointerCapture } from "@/utils/dom"
const props = defineProps<{
......@@ -16,6 +16,7 @@ const emit = defineEmits(["close", "hide"])
const sidebarHtml = chrome.runtime.getURL("sidebar.html")
const sidebarUrl = sidebarHtml + "?mode=content"
const logoUrl = chrome.runtime.getURL("/logo.svg")
const extRootUrl = chrome.runtime.getURL("/")
let sheet: CSSStyleSheet | null = null
let invisibleTimer = 0
......@@ -62,6 +63,21 @@ function handleKeyUp(e: KeyboardEvent) {
}
}
function handleMessage(e: MessageEvent) {
if (!extRootUrl.startsWith(e.origin)) {
return
}
// console.log(e)
switch (e.data?.type) {
case FrameMessageType.collapseSidebar:
emit("hide")
break
case FrameMessageType.closeSidebar:
emit("close")
break
}
}
watch(
width,
debounce((value) => {
......@@ -90,6 +106,7 @@ onMounted(() => {
updatePageStyle(width.value)
window.addEventListener("keydown", handleKeydown)
window.addEventListener("keyup", handleKeyUp)
window.addEventListener("message", handleMessage)
chrome.runtime.sendMessage({
type: MessageType.registerContentSidebar,
})
......@@ -99,6 +116,7 @@ onUnmounted(() => {
disablePageStyle()
window.removeEventListener("keydown", handleKeydown)
window.removeEventListener("keyup", handleKeyUp)
window.removeEventListener("message", handleMessage)
chrome.runtime.sendMessage({
type: MessageType.unregisterContentSidebar,
})
......@@ -121,7 +139,10 @@ onUnmounted(() => {
@pointerdown="autoPointerCapture"
@pointermove="pointermove"
></div>
<div class="flex gap-2 items-center justify-between h-8 px-2">
<div
v-if="false"
class="flex gap-2 items-center justify-between h-8 px-2"
>
<button
@click=""
class="size-7 flex items-center justify-center mr-auto"
......
......@@ -31,9 +31,10 @@ const emit = defineEmits({
<div class="flex flex-wrap gap-x-4 gap-y-4 justify-center">
<SiteButton
v-for="item of recentItems"
v-for="item of recentItems.slice(0, 12)"
:icon="item.icon"
:title="item.title"
:url="item.url"
badge="remove"
@click="emit('go', item.url)"
@remove="emit('removeRecentItem', item.url)"
......
......@@ -6,15 +6,20 @@ import {
pipWindow,
sidebarAddon,
} from "@/store/content"
import { ContentEventType, FrameMessageType, MessageType } from "@/types"
import {
ContentEventType,
FrameMessageType,
MessageType,
WebviewFunc,
WindowName,
} from "@/types"
import Copilot from "./Copilot.vue"
import { waitMessage } from "@/utils/ext"
import {
dispatchContentEvent,
addContentEventListener,
removeContentEventListener,
} from "@/content/event"
import { contentService } from "@/utils/service"
import { contentInvoke } from "@/utils/invoke"
import { getPageIcon } from "@/utils/dom"
// import { PipEventName } from "@/types/pip"
......@@ -47,7 +52,7 @@ function handleMessage(
})
break
case MessageType.invokeResponse:
contentService.handleMessage(message)
contentInvoke.handleResMsg(message)
break
case MessageType.showChatDocs:
chatDocsPanel.visible = true
......@@ -150,18 +155,50 @@ function handleFrameMessage(e: MessageEvent) {
type: ContentEventType.escapeLoad,
detail: { url: e.data.url },
})
case FrameMessageType.reload:
return location.reload()
case FrameMessageType.goBack:
return history.back()
case FrameMessageType.goForward:
return history.forward()
case FrameMessageType.invokeRequest:
handleInvokeRequest(e.data, e.source as Window)
break
}
}
async function handleInvokeRequest(message: any, source: Window) {
const { key, func, args } = message
let result = null
let error = null
try {
switch (func) {
case WebviewFunc.reload:
location.reload()
break
case WebviewFunc.goBack:
history.back()
break
case WebviewFunc.goForward:
history.forward()
break
}
} catch (err) {
console.error("invoke error: ", err)
error = err
}
console.log("invoke response: ", result, error)
source.postMessage(
{
type: FrameMessageType.invokeResponse,
key,
success: !error,
payload: !error ? result : error,
},
chrome.runtime.getURL("")
)
}
if (window.self == window.parent) {
if (window.top === window) {
run()
} else {
}
// webview
if (window.top !== window && window.name.startsWith(WindowName.webview)) {
window.addEventListener("message", handleFrameMessage)
window.parent?.postMessage(
{
......
import { ContentEventType } from "@/types"
import { ContentEventType, WindowName } from "@/types"
import { addContentEventListener } from "./event"
import { copilotNavigateTo, pip, fetchDoc, writeHtml } from "./pip"
......@@ -27,3 +27,8 @@ async function handleEscapeLoadEvent(event: CustomEvent | Event) {
addContentEventListener(ContentEventType.pip, handlePipEvent)
addContentEventListener(ContentEventType.pipLoad, handlePipLoadDocEvent)
addContentEventListener(ContentEventType.escapeLoad, handleEscapeLoadEvent)
if (window.name.startsWith(WindowName.webview)) {
window.parent = window
window.top = window
}
......@@ -21,6 +21,7 @@ import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue"
import SiteButton from "@/components/SiteButton.vue"
import { getIsEdge } from "@/utils/ext"
import { handleImgError } from "@/utils/dom"
import { homeUrl, feedbackUrl } from "@/utils/const"
const globeImg = chrome.runtime.getURL("img/globe.svg")
......@@ -151,7 +152,7 @@ async function openSidebar(url = "") {
})
const win = await chrome.windows.getCurrent()
await chrome.storage.session.set({
sidebarUrls: { sidepanel: url },
sidebarInitUrl: { sidepanel: url },
})
await chrome.sidePanel.open({ windowId: win.id! })
......@@ -160,7 +161,7 @@ async function openSidebar(url = "") {
async function openContentSidebar(url = "") {
await chrome.storage.session.set({
sidebarUrls: { content: url },
sidebarInitUrl: { content: url },
})
await chrome.tabs.sendMessage(activeTab.value.id!, {
type: MessageType.openContentSidebar,
......@@ -180,7 +181,7 @@ async function handleClickLaunch(url: string) {
}
function feedback() {
open("https://tawk.to/anythingcopilot", "_blank")
open(feedbackUrl, "_blank")
}
function fivestar() {
open(
......@@ -194,7 +195,7 @@ function fivestar() {
}
function goHome() {
open("https://ziziyi.com/copilot", "_blank")
open(homeUrl, "_blank")
}
function showChatDocs() {
......@@ -347,6 +348,7 @@ function showChatDocs() {
:icon="item.icon"
:title="item.title"
:badge="keyboard.shift || (isEdge && !avaiable) ? 'popup' : 'sidebar'"
:url="item.url"
small
@click="
() => {
......
<script setup lang="ts">
import { ref, onMounted, reactive, computed, onUnmounted } from "vue"
import { getLocal, getSession, isProtectedUrl } from "@/utils/ext"
import { ref, onMounted, reactive, computed, onUnmounted, watch } from "vue"
import {
getLocal,
getSession,
isProtectedUrl,
updateFrameNetRules,
updateUANetRules,
} from "@/utils/ext"
import config from "@/assets/config.json"
import Webview from "@/components/Webview.vue"
import Webview, { type PageInfo } from "@/components/Webview.vue"
import { useI18n } from "@/utils/i18n"
import { MessageType } from "@/types"
import SidebarHome from "@/components/sidebar/SidebarHome.vue"
......@@ -12,22 +18,138 @@ import IconNavigateBefore from "@/components/icons/IconNavigateBefore.vue"
import IconNavigateNext from "@/components/icons/IconNavigateNext.vue"
import IconRefresh from "@/components/icons/IconRefresh.vue"
import IconHome from "@/components/icons/IconHome.vue"
import IconPhone from "@/components/icons/IconPhone.vue"
import { handleImgError } from "@/utils/dom"
import { FrameMessageType } from "@/types"
import { homeUrl } from "@/utils/const"
const logoUrl = chrome.runtime.getURL("/logo.svg")
const globeImg = chrome.runtime.getURL("img/globe.svg")
const { t } = useI18n()
const mobileUA =
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
const url = ref("")
const mode = ref("")
const popularItems = reactive(config.data.popularSites)
const recentItems = reactive<{ url: string; title: string; icon: string }[]>([])
const currentTab = reactive({
tabId: 0,
tabId: -1,
})
const ua = ref("")
const webview = ref<InstanceType<typeof Webview> | null>(null)
const defaultUA = ref(config.data.embedView.defaultUA)
const patchs = reactive(config.data.webviewPatchs)
const preloadUrls = reactive(config.data.loadCandidates)
const protectedUrl = computed(() => {
return isProtectedUrl(url.value)
const pages = reactive<{ url: string }[]>([])
const webviews = ref<InstanceType<typeof Webview>[]>([])
const webviewsAttr = computed(() => {
return pages.map(({ url }) => {
const patch = patchs.find((i) => new RegExp(i.re).test(url))
return {
url: url,
preloadUrl: patch?.l || "",
preloadCandidates: preloadUrls,
}
})
})
const active = ref(-1)
const pagesInfo = reactive<{ [index: number]: PageInfo }>({})
const hostUA = reactive<Record<string, number>>(config.data.embedView.hostUA)
const isPointerIn = ref(false)
const isMobileUA = computed(() => {
if (active.value === -1) {
return /Mobile/.test(defaultUA.value)
}
const url = pagesInfo[active.value]?.url || pages[active.value].url
const u = new URL(url)
if (u.host in hostUA) {
return hostUA[u.host] === 1
}
const parent = Object.keys(hostUA).find((k) => {
return u.host.endsWith(k)
})
console.log("parent", hostUA, u.host, parent)
if (parent) {
return hostUA[parent] === 1
}
return /Mobile/.test(defaultUA.value)
})
watch(isPointerIn, () => {
updateNetRules()
})
onMounted(() => {
const q = new URLSearchParams(location.search)
mode.value = q.get("mode") || "sidepanel"
// ua.value =
// "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
getLocal({
popularSites: config.data.popularSites,
sidebarRecentItems: [],
webviewPatchs: config.data.webviewPatchs,
loadCandidates: config.data.loadCandidates,
presetEmbedView: config.data.embedView,
customEmbedView: { defaultUA: "", hostUA: {} },
}).then(
({
popularSites,
sidebarRecentItems,
webviewPatchs,
loadCandidates,
presetEmbedView,
customEmbedView,
}) => {
if (popularSites) {
popularItems.splice(0, popularItems.length, ...popularSites)
}
if (sidebarRecentItems) {
recentItems.splice(0, recentItems.length, ...sidebarRecentItems)
}
if (webviewPatchs) {
patchs.splice(0, patchs.length, ...webviewPatchs)
}
if (loadCandidates) {
preloadUrls.splice(0, preloadUrls.length, ...loadCandidates)
}
defaultUA.value = customEmbedView.defaultUA || presetEmbedView.defaultUA
Object.entries({
...presetEmbedView.hostUA,
...customEmbedView.hostUA,
}).map(([k, v]) => {
hostUA[k] = v
})
}
)
getSession({
sidebarInitUrl: {} as Record<string, string>,
}).then(({ sidebarInitUrl }) => {
console.log("[sidebar]", sidebarInitUrl, mode.value)
const url = sidebarInitUrl?.[mode.value]
if (url && !isProtectedUrl(url)) {
go(url)
}
})
chrome.tabs.getCurrent().then((t) => {
currentTab.tabId = t?.id || -1
})
chrome.runtime.onMessage.addListener(handleMessage)
})
onUnmounted(() => {
chrome.runtime.onMessage.removeListener(handleMessage)
})
async function handleMessage(message: any) {
......@@ -38,30 +160,25 @@ async function handleMessage(message: any) {
currentTab.tabId = current?.id || -1
}
if (message.tabId == currentTab.tabId) {
url.value = message.url
const url = message.url
go(url)
}
break
}
}
async function updateRecentItems(pageInfo: {
url: string
title: string
icon: string
}) {
async function handlePageLoad(i: number, pageInfo: PageInfo) {
pagesInfo[i] = pageInfo
if (mode.value === "content") {
chrome.runtime.sendMessage({
type: MessageType.registerContentSidebar,
url: url.value,
pages: pages,
})
}
const { sidebarRecentItems } = await getLocal({
sidebarRecentItems: [] as {
url: string
icon: string
title: string
}[],
sidebarRecentItems: [] as PageInfo[],
})
const index = sidebarRecentItems.findIndex((i) => i.url === pageInfo.url)
......@@ -69,7 +186,7 @@ async function updateRecentItems(pageInfo: {
sidebarRecentItems.splice(index, 1)
}
sidebarRecentItems.unshift(pageInfo)
sidebarRecentItems.splice(12)
sidebarRecentItems.splice(36)
recentItems.splice(0, recentItems.length, ...sidebarRecentItems)
await chrome.storage.local.set({ sidebarRecentItems })
......@@ -92,105 +209,258 @@ async function removeRecentItem(url: string) {
await chrome.storage.local.set({ sidebarRecentItems })
}
onMounted(() => {
const q = new URLSearchParams(location.search)
mode.value = q.get("mode") || "sidepanel"
// ua.value =
// "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
function go(link: string) {
const len = pages.push({ url: link })
active.value = len - 1
}
getLocal({
popularSites: config.data.popularSites,
sidebarRecentItems: [],
}).then(({ popularSites, sidebarRecentItems }) => {
if (popularSites) {
popularItems.splice(0, popularItems.length, ...popularSites)
}
if (sidebarRecentItems) {
recentItems.splice(0, recentItems.length, ...sidebarRecentItems)
}
function goBack() {
const webview = webviews.value[active.value]
webview?.goBack()
}
function goForward() {
const webview = webviews.value[active.value]
webview?.goForward()
}
function reload() {
const webview = webviews.value[active.value]
webview?.reload()
}
function closeWebview(index: number) {
pages.splice(index, 1)
delete pagesInfo[index]
if (active.value > pages.length - 1) {
active.value = pages.length - 1
}
}
function collapseSidebar() {
if (mode.value == "content") {
parent.postMessage({ type: FrameMessageType.collapseSidebar }, "*")
}
}
function closeSidebar() {
if (mode.value == "content") {
parent.postMessage({ type: FrameMessageType.closeSidebar }, "*")
} else {
window.close()
}
}
async function toggleMobileUA() {
if (active.value === -1) {
defaultUA.value = isMobileUA.value ? "" : mobileUA
return
}
const url = pagesInfo[active.value]?.url || pages[active.value].url
const u = new URL(url)
hostUA[u.host] = isMobileUA.value ? 0 : 1
await updateNetRules()
reload()
}
async function updateNetRules() {
console.log("isPointerIn", defaultUA.value)
const tabId = currentTab.tabId
const extId = chrome.runtime.id
await updateFrameNetRules({
id: 2,
ua: defaultUA.value,
tabIds: [tabId],
enabled: isPointerIn.value,
})
getSession({
sidebarUrls: {} as Record<string, string>,
}).then(({ sidebarUrls }) => {
console.log("[sidebar]", sidebarUrls, mode.value)
if (sidebarUrls && sidebarUrls[mode.value]) {
url.value = sidebarUrls[mode.value]
const mobileHosts = ["mobile.ziziyi.com"]
const desktopHosts = ["desktop.ziziyi.com"]
Object.entries(hostUA).forEach(([k, v]) => {
if (v === 1) {
mobileHosts.push(k)
} else {
desktopHosts.push(k)
}
})
chrome.tabs.getCurrent().then((t) => {
currentTab.tabId = t?.id || -1
await updateUANetRules({
id: 3,
ua: mobileUA,
requestDomains: mobileHosts,
tabIds: [tabId],
enabled: isPointerIn.value,
})
await updateUANetRules({
id: 4,
ua: navigator.userAgent,
requestDomains: desktopHosts,
tabIds: [tabId],
enabled: isPointerIn.value,
})
}
chrome.runtime.onMessage.addListener(handleMessage)
})
let timer = 0
onUnmounted(() => {
chrome.runtime.onMessage.removeListener(handleMessage)
})
function handlePointerEnter() {
isPointerIn.value = true
clearTimeout(timer)
}
function go(link: string) {
url.value = link
function handlePointerLeave() {
timer = window.setTimeout(() => {
isPointerIn.value = false
}, 350)
}
</script>
<template>
<div class="w-full h-screen flex flex-col">
<div
class="w-full h-screen flex flex-col"
@pointerenter="handlePointerEnter"
@pointerleave="handlePointerLeave"
>
<div
:class="[
'flex gap-1 items-center justify-between h-8 px-1',
'*:size-7 *:rounded-full *:flex *:items-center *:justify-center ',
'flex gap-1 items-center justify-between h-9 px-1 z-10 shadow-sm',
'*:size-7 *:flex *:items-center *:justify-center ',
]"
>
<button @click="" class="group hover:bg-background-soft mr-auto">
<!-- <img class="size-4" :src="logoUrl" /> -->
<IconHome class="size-5 group-active:scale-90 transition-transform" />
</button>
<button class="group hover:bg-background-soft" @click="webview?.goBack()">
<a
@click="(e) => (e.preventDefault(), (active = -1))"
:href="homeUrl"
:class="[
'group rounded-lg relative box-border border',
active === -1
? 'bg-primary/10 border-primary-500'
: 'border-transparent hover:bg-background-mute bg-background-soft',
]"
>
<img
:class="[
'size-4 group-hover:opacity-85 transition-all pointer-events-none',
false ? 'opacity-85' : 'opacity-50',
]"
:src="logoUrl"
/>
<!-- <IconHome class="size-5 group-active:scale-90 transition-transform" /> -->
</a>
<a
v-for="(page, i) of pages"
@click="(e) => (e.preventDefault(), (active = i))"
:href="page.url"
:class="[
'group rounded-lg relative box-border border',
active === i
? 'bg-primary/10 border-primary-500'
: 'border-transparent hover:bg-background-mute bg-background-soft',
]"
>
<img
class="size-5 scale-90 pointer-events-none"
loading="lazy"
:src="pagesInfo[i]?.icon || globeImg"
:data-fallback="globeImg"
@error="handleImgError"
/>
<span
v-if="active === i"
class="absolute hidden group-hover:block rounded-full bg-primary-500 text-white -top-0.5 -right-0.5 transition-all p-0.5"
@click="
(e) => {
e.preventDefault()
e.stopPropagation()
closeWebview(i)
}
"
>
<IconClose class="size-2" />
</span>
</a>
<span class="mx-auto"></span>
<!-- <button
@click="goBack()"
class="group hover:bg-background-soft rounded-full"
>
<IconNavigateBefore
class="size-5 scale-125 group-active:-translate-x-1 transition-transform"
/>
</button>
<button
class="group hover:bg-background-soft"
@click="webview?.goForward()"
@click="goForward()"
class="group hover:bg-background-soft rounded-full"
>
<IconNavigateNext
class="size-5 scale-125 group-active:translate-x-1 transition-transform"
/>
</button>
<button class="group hover:bg-background-soft" @click="webview?.reload()">
</button> -->
<button
@click="reload()"
class="group hover:bg-background-soft rounded-full"
>
<IconRefresh
class="size-5 group-active:rotate-180 transition-transform"
/>
</button>
<button class="group hover:bg-background-soft">
<button
@click="toggleMobileUA"
:class="[
'group hover:bg-background-soft rounded-full transition ease-in-out delay-200',
{ 'bg-background-soft text-primary-500 ': isMobileUA },
isPointerIn ? '' : ' opacity-75',
]"
>
<IconPhone class="size-4 group-active:scale-90 transition-transform" />
</button>
<button
v-if="mode == 'content'"
@click="collapseSidebar()"
class="group hover:bg-background-soft rounded-full"
>
<IconSplitscreenRight
class="size-5 scale-95 group-active:scale-90 transition-transform"
/>
</button>
<button class="group hover:bg-background-soft">
<button
v-if="mode == 'content'"
@click="closeSidebar()"
class="group hover:bg-background-soft rounded-full"
>
<IconClose class="size-5 group-active:scale-90 transition-transform" />
</button>
</div>
<div class="w-full h-full" v-if="!protectedUrl">
<Webview
ref="webview"
:url="url"
:ua="ua"
@page-info="updateRecentItems"
<div :class="['w-full h-full', { hidden: active != -1 }]">
<SidebarHome
:recentItems="recentItems"
:popularItems="popularItems"
@go="go"
@remove-recent-item="removeRecentItem"
/>
</div>
<SidebarHome
v-else
:recentItems="recentItems"
:popularItems="popularItems"
@go="go"
@remove-recent-item="removeRecentItem"
/>
<div
v-for="(attr, i) of webviewsAttr"
:class="[
'w-full h-full rounded-sm overflow-hidden',
{ hidden: active != i },
]"
>
<Webview
ref="webviews"
:key="i"
:url="attr.url"
:ua="defaultUA"
:preload-url="attr.preloadUrl"
:preload-candidates="attr.preloadCandidates"
@load="(info) => handlePageLoad(i, info)"
/>
</div>
</div>
<!-- <LoadingBar /> -->
......
......@@ -54,7 +54,18 @@ export enum FrameMessageType {
webviewRun = "anything-copilot_webview-run",
escapeLoad = "anything-copilot_escape-load",
pageInfo = "anything-copilot_page-info",
reload = "anything-copilot_reload",
goBack = "anything-copilot_go-back",
goForward = "anything-copilot_go-forward",
collapseSidebar = "anything-copilot_collapse-sidebar",
closeSidebar = "anything-copilot_close-sidebar",
invokeRequest = "anything-copilot_invoke-request",
invokeResponse = "anything-copilot_invoke-response",
}
export enum WindowName {
webview = "anything-copilot_webview",
}
export enum WebviewFunc {
reload = "reload",
goBack = "goBack",
goForward = "goForward",
}
export const chatDocPrompt = `Here is some relevant reference information. Please refrain from summarizing or responding to specific content until I pose targeted questions. If you comprehend this instruction, kindly reply with "Okay, please continue."`
export const homeUrl = 'https://ziziyi.com/copilot'
export const feedbackUrl = 'https://tawk.to/anythingcopilot'
......@@ -217,7 +217,7 @@ export async function waitFor(
}
export function getPageIcon() {
const icons = document.querySelectorAll<HTMLLinkElement>('link[rel="icon"]')
const icons = document.querySelectorAll<HTMLLinkElement>('link[rel~="icon"]')
for (let item of icons) {
if (item.getAttribute("type") == "image/svg+xml") {
return item.href
......@@ -231,6 +231,13 @@ export function getPageIcon() {
return icons[0].href
}
const touchIcon = document.querySelector<HTMLLinkElement>(
'link[rel="apple-touch-icon"]'
)
if (touchIcon) {
return touchIcon.href
}
return location.origin + "/favicon.ico"
}
......
......@@ -185,56 +185,116 @@ export function getSession<T extends Record<string, any>>(key: string | T) {
}
type NetRulesOptions = {
id?: number
ua?: string
tabIds?: number[]
requestDomains?: string[]
initiatorDomains?: string[]
enabled?: boolean
}
export async function updateFrameNetRules(
{ ua, tabIds, initiatorDomains }: NetRulesOptions = {
tabIds: [-1],
initiatorDomains: [chrome.runtime.id],
}
) {
export async function updateFrameNetRules({
id,
ua,
tabIds,
initiatorDomains,
requestDomains,
enabled,
}: NetRulesOptions) {
ua = ua || navigator.userAgent
// const noneTab = tabIds?.length == 1 && tabIds[0] == -1
// if (!initiatorDomains && !noneTab) {
// initiatorDomains = [chrome.runtime.id]
// }
await chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [1],
addRules: [
{
id: 1,
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,
responseHeaders: [
{
header: "X-Frame-Options",
operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE,
},
removeRuleIds: [id || 1],
addRules:
enabled != false
? [
{
header: "Content-Security-Policy",
operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE,
},
],
requestHeaders: [
{
header: "User-Agent",
value: ua,
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
id: id || 1,
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType
.MODIFY_HEADERS,
responseHeaders: [
{
header: "X-Frame-Options",
operation:
chrome.declarativeNetRequest.HeaderOperation.REMOVE,
},
{
header: "Content-Security-Policy",
operation:
chrome.declarativeNetRequest.HeaderOperation.REMOVE,
},
],
requestHeaders: [
{
header: "User-Agent",
value: ua,
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
},
{
header: "Sec-Fetch-Dest",
value: "document",
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
},
],
},
condition: {
// resourceTypes: [chrome.declarativeNetRequest.ResourceType.SUB_FRAME],
initiatorDomains,
requestDomains,
tabIds,
},
},
]
: [],
})
}
export async function updateUANetRules({
id,
ua,
requestDomains,
initiatorDomains,
tabIds,
enabled,
}: NetRulesOptions & { id: number }) {
// const noneTab = tabIds?.length == 1 && tabIds[0] == -1
// if (!initiatorDomains && !noneTab) {
// initiatorDomains = [chrome.runtime.id]
// }
await chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [id],
addRules:
enabled != false
? [
{
header: "Sec-Fetch-Dest",
value: "document",
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
id,
priority: 2,
action: {
type: chrome.declarativeNetRequest.RuleActionType
.MODIFY_HEADERS,
requestHeaders: [
{
header: "User-Agent",
value: ua,
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
},
],
},
condition: {
// resourceTypes: [chrome.declarativeNetRequest.ResourceType.SUB_FRAME],
initiatorDomains,
requestDomains,
tabIds,
},
},
],
},
condition: {
resourceTypes: [chrome.declarativeNetRequest.ResourceType.SUB_FRAME],
initiatorDomains,
tabIds,
},
},
],
]
: [],
})
}
......@@ -250,12 +310,13 @@ export function isProtectedUrl(url: string) {
}
const isEdge = getIsEdge()
if (isEdge && u.hostname == "microsoftedge.microsoft.com") {
return true
}
if (u.hostname == "chrome.google.com") {
if (
["chromewebstore.google.com", "chrome.google.com"].includes(u.hostname)
) {
return true
}
......
import { MessageType, ServiceFunc, type ParseDocOptions } from "@/types"
import Invoke from "./Invoke"
class Service extends Invoke {
class ContentInvoke extends Invoke {
public async send(req: any) {
const key = this.key
const response = await chrome.runtime.sendMessage({
......@@ -13,7 +13,7 @@ class Service extends Invoke {
return { key, response }
}
public handleMessage(message: any) {
public handleResMsg(message: any) {
if (message?.type === MessageType.invokeResponse) {
const { key, success, payload } = message
this.setReturnValue(key, success, payload)
......@@ -42,6 +42,6 @@ class Service extends Invoke {
}
}
const contentService = new Service("content")
const contentInvoke = new ContentInvoke("content")
export { Service, contentService }
export { ContentInvoke, contentInvoke }
......@@ -6,6 +6,13 @@ type CallbackItem = {
interface InvokeReq {
func: string
args: any[]
timeout?: number
}
interface InvokeRes {
key: string
success: boolean
value: any
}
abstract class Invoke {
......@@ -25,14 +32,14 @@ abstract class Invoke {
return `${this.name}-${this.uniqueId}-${this.count++}`
}
protected getReturnValue(key: string, req: InvokeReq, timeout?: number) {
protected getReturnValue(key: string, req: InvokeReq) {
let timer: any
const { func, timeout } = req
const promise = new Promise<any>((resolve, reject) => {
this.pendingCallback[key] = { resolve, reject }
if (timeout) {
timer = setTimeout(
() =>
reject(`"${this.name}" invoke timeout: ${req.func} key: ${key}`),
() => reject(`"${this.name}" invoke timeout: ${func} key: ${key}`),
timeout
)
}
......@@ -46,11 +53,11 @@ abstract class Invoke {
return promise
}
protected setReturnValue(key: string, success: boolean, payload: any) {
protected setReturnValue(key: string, success: boolean, value: any) {
const callback = this.pendingCallback[key]
if (callback) {
const fn = success != false ? callback.resolve : callback.reject
fn(payload)
fn(value)
} else {
console.error(`unknown invoke callback message: ${key}`)
console.log(this.pendingCallback)
......@@ -58,11 +65,12 @@ abstract class Invoke {
}
abstract send(req: InvokeReq): Promise<{ key: string; response: any }>
abstract handleMessage(message: InvokeReq): void
abstract handleResMsg(message: InvokeRes): void
public async invoke<T = any>(req: InvokeReq, timeout = 20000): Promise<T> {
public async invoke<T = any>(req: InvokeReq): Promise<T> {
const { key } = await this.send(req)
return this.getReturnValue(key, req, timeout)
req.timeout = req.timeout || 20000
return this.getReturnValue(key, req)
}
}
......
import { FrameMessageType } from "@/types"
import Invoke from "./Invoke"
class WebviewInvoke extends Invoke {
private frame: HTMLIFrameElement | null = null
constructor(name: string, frame: HTMLIFrameElement) {
super(name)
this.frame = frame
}
public async send(req: any) {
const key = this.key
const win = this.frame?.contentWindow
if (!win) {
console.warn("WebviewInvoke: frame not ready", this.frame)
}
win?.postMessage({ type: FrameMessageType.invokeRequest, key, ...req }, "*")
return { key, response: null }
}
public handleResMsg(message: any) {
if (message?.type === FrameMessageType.invokeResponse) {
const { key, success, payload } = message
this.setReturnValue(key, success, payload)
}
}
}
export { WebviewInvoke }
export { Invoke } from "./Invoke"
export { ContentInvoke, contentInvoke } from "./ContentInvoke"
export { WebviewInvoke } from "./WebviewInvoke"
......@@ -52,7 +52,10 @@ export const semanticClip = (text: string, maxLength: number) => {
}
/** Find URL from the same origin that can be embedded */
export async function findFrameLoadUrl(urls: string[]): Promise<string> {
export async function findFrameLoadUrl(
urls: string[],
base?: string
): Promise<string> {
const abortController = new AbortController()
let resolve: null | ((url: string) => void) = null
......@@ -65,8 +68,9 @@ export async function findFrameLoadUrl(urls: string[]): Promise<string> {
return !csp.includes("frame-ancestors")
}
const promises = urls.map((url) =>
fetch(url, {
const promises = urls.map((url) => {
const u = new URL(url, base)
return fetch(u.href, {
signal: abortController.signal,
})
.then((res) => {
......@@ -74,12 +78,12 @@ export async function findFrameLoadUrl(urls: string[]): Promise<string> {
const xFrameOptions = h.get("X-Frame-Options")
const csp = h.get("Content-Security-Policy")
if (!xFrameOptions && checkCSP(csp)) {
resolve && resolve(url)
resolve && resolve(u.href)
abortController.abort()
}
})
.catch(() => {})
)
})
Promise.all(promises).then(() => {
resolve && resolve("")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment