Commit 1ee1ea77 authored by Domi's avatar Domi

feat: sidebar multitab

parent f1552320
...@@ -18,8 +18,10 @@ ...@@ -18,8 +18,10 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mammoth": "^1.6.0", "mammoth": "^1.6.0",
"pdfjs-dist": "^4.0.269", "pdfjs-dist": "^4.0.269",
"punycode.js": "^2.3.1",
"tiktoken": "^1.0.11", "tiktoken": "^1.0.11",
"turndown": "^7.1.2", "turndown": "^7.1.2",
"ua-parser-js": "^1.0.37",
"vite-plugin-top-level-await": "^1.4.1", "vite-plugin-top-level-await": "^1.4.1",
"vite-plugin-wasm": "^3.3.0", "vite-plugin-wasm": "^3.3.0",
"vue": "^3.3.4", "vue": "^3.3.4",
...@@ -8752,6 +8754,14 @@ ...@@ -8752,6 +8754,14 @@
"pump": "^2.0.0" "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": { "node_modules/q": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
...@@ -10273,6 +10283,28 @@ ...@@ -10273,6 +10283,28 @@
"node": ">=14.17" "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": { "node_modules/uberproto": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/uberproto/-/uberproto-1.2.0.tgz", "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> <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 \ No newline at end of file
{ {
"data": { "data": {
"configVersion": 20240310, "configVersion": 20240330,
"chatDocSites": [ "chatDocSites": [
{ {
"host": "huggingface.co", "host": "huggingface.co",
...@@ -135,6 +135,13 @@ ...@@ -135,6 +135,13 @@
], ],
"search": { "search": {
"defaultEngine": "google" "defaultEngine": "google"
},
"embedView": {
"defaultUA": "",
"hostUA": {
"www.google.com": 1,
"bing.com": 1
}
} }
} }
} }
...@@ -6,13 +6,13 @@ ...@@ -6,13 +6,13 @@
font-weight: normal; font-weight: normal;
} }
a, /* a,
.green { .green {
text-decoration: none; text-decoration: none;
/* color: hsla(160, 100%, 37%, 1); */ color: hsla(160, 100%, 37%, 1);
color: #38bdf8; color: #38bdf8;
transition: 0.4s; transition: 0.4s;
} } */
@media (hover: hover) { @media (hover: hover) {
a:hover { a:hover {
......
...@@ -164,7 +164,7 @@ function handleMessage(message: any, sender: chrome.runtime.MessageSender) { ...@@ -164,7 +164,7 @@ function handleMessage(message: any, sender: chrome.runtime.MessageSender) {
case MessageType.setupOffscreenDocument: case MessageType.setupOffscreenDocument:
return offscreen.setup() return offscreen.setup()
case MessageType.fromOffscreen: case MessageType.fromOffscreen:
return offscreen.handleMessage(message) return offscreen.handleResMsg(message)
case MessageType.invokeRequest: case MessageType.invokeRequest:
handleInvokeRequest(message, sender) handleInvokeRequest(message, sender)
break break
......
import { MessageType, ServiceFunc, type ParseDocOptions } from "@/types" 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 let creating: Promise<void> | null // A global promise to avoid concurrency issues
...@@ -60,7 +60,7 @@ class Offscreen extends Invoke { ...@@ -60,7 +60,7 @@ class Offscreen extends Invoke {
return { key, response } return { key, response }
} }
public handleMessage(message: any): void { public handleResMsg(message: any): void {
const { type, key, payload, success } = message const { type, key, payload, success } = message
if (type === MessageType.fromOffscreen) { if (type === MessageType.fromOffscreen) {
this.setReturnValue(key, success, payload) this.setReturnValue(key, success, payload)
......
...@@ -19,7 +19,7 @@ export async function handleContentMounted(tabId: number) { ...@@ -19,7 +19,7 @@ export async function handleContentMounted(tabId: number) {
const sidebar = contentSidebarMap.get(tabId) const sidebar = contentSidebarMap.get(tabId)
if (sidebar) { if (sidebar) {
await chrome.storage.session.set({ await chrome.storage.session.set({
sidebarUrls: { content: sidebar.url }, sidebarInitUrl: { content: sidebar.url },
}) })
chrome.tabs.sendMessage(tabId, { chrome.tabs.sendMessage(tabId, {
type: MessageType.openContentSidebar, type: MessageType.openContentSidebar,
......
...@@ -32,6 +32,7 @@ const engine = ref(Engine.google) ...@@ -32,6 +32,7 @@ const engine = ref(Engine.google)
const engineListVisible = ref(false) const engineListVisible = ref(false)
let timerId = -1 let timerId = -1
let timerId2 = -1
watch(focus, (value) => { watch(focus, (value) => {
if (value) { if (value) {
...@@ -103,20 +104,37 @@ function switchEngine(value: Engine) { ...@@ -103,20 +104,37 @@ function switchEngine(value: Engine) {
engine.value = value engine.value = value
chrome.storage.local.set({ searchEngine: value }) chrome.storage.local.set({ searchEngine: value })
} }
function handlePointerLeave() {
timerId2 = window.setTimeout(() => {
engineListVisible.value = false
}, 800)
}
function handlePointerEnter() {
clearTimeout(timerId2)
}
</script> </script>
<template> <template>
<div class="relative h-10 w-full"> <div class="relative h-10 w-full">
<div <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"> <div aria-label="search box" class="flex px-2 items-center">
<button <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" @click="engineListVisible = !engineListVisible"
> >
<img <img
class="size-5" class="size-5 group-active:scale-90 transition-transform"
:src="engine == Engine.bing ? bingImg : googleImg" :src="engine == Engine.bing ? bingImg : googleImg"
/> />
</button> </button>
......
...@@ -11,29 +11,32 @@ defineProps<{ ...@@ -11,29 +11,32 @@ defineProps<{
title: string title: string
badge?: "popup" | "sidebar" | "remove" badge?: "popup" | "sidebar" | "remove"
small?: boolean small?: boolean
url?: string
}>() }>()
defineEmits(["click", "remove"]) defineEmits(["click", "remove"])
</script> </script>
<template> <template>
<button <a
:class="[ :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', 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"> <div class="p-2.5 rounded-full bg-background-soft">
<img <img
class="size-6 rounded" class="size-6 rounded pointer-events-none"
:src="icon" :src="icon"
:data-fallback="globeImg" :data-fallback="globeImg"
loading="lazy" loading="lazy"
@error="handleImgError" @error="handleImgError"
/> />
</div> </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"> <div class="text-xs max-w-full break-words leading-tight line-clamp-2">
{{ title }} {{ title }}
</div> </div>
...@@ -64,7 +67,7 @@ defineEmits(["click", "remove"]) ...@@ -64,7 +67,7 @@ defineEmits(["click", "remove"])
> >
<IconClose class="size-3" /> <IconClose class="size-3" />
</div> </div>
</button> </a>
</template> </template>
<style scoped></style> <style scoped></style>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, watch, onMounted, computed, onUnmounted } from "vue" import { ref, reactive, watch, onMounted, computed, onUnmounted } from "vue"
import config from "@/assets/config.json" import { updateFrameNetRules } from "@/utils/ext"
import { getLocal, updateFrameNetRules } from "@/utils/ext" import {
import { ContentScriptId, FrameMessageType } from "@/types" ContentScriptId,
FrameMessageType,
WindowName,
WebviewFunc,
} from "@/types"
import { findFrameLoadUrl } from "@/utils/utils" import { findFrameLoadUrl } from "@/utils/utils"
import { fetchDoc } from "@/content/pip" import { fetchDoc } from "@/content/pip"
import { WebviewInvoke } from "@/utils/invoke"
export type PageInfo = {
url: string
title: string
icon: string
}
const props = defineProps<{ const props = defineProps<{
url: string url: string
ua?: string ua?: string
preloadUrl?: string
preloadCandidates?: string[]
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
pageInfo: [pageInfo: { url: string; title: string; icon: string }] load: [PageInfo]
}>() }>()
defineExpose({ defineExpose({
...@@ -24,59 +37,42 @@ defineExpose({ ...@@ -24,59 +37,42 @@ defineExpose({
const onceCallback = new Map<string, (e: MessageEvent) => boolean>() const onceCallback = new Map<string, (e: MessageEvent) => boolean>()
const frame = ref<HTMLIFrameElement>() const frame = ref<HTMLIFrameElement>()
const patchs = reactive(config.data.webviewPatchs)
const loadUrls = reactive(config.data.loadCandidates)
const inited = ref(false)
const frameUrl = ref("") const frameUrl = ref("")
const pageInfo = reactive({ url: "", title: "", icon: "" }) 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(() => { onMounted(() => {
getLocal({ webviewInvoke.value = new WebviewInvoke("webview", frame.value!)
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
})
window.addEventListener("message", handleFrameMessage) window.addEventListener("message", handleFrameMessage)
console.log("loadFrame", props.url, props.ua)
loadFrame(props.url, props.ua)
}) })
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("message", handleFrameMessage) window.removeEventListener("message", handleFrameMessage)
}) })
const patch = computed(() => { async function loadFrame(url: string, ua?: string) {
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) => {
const iframe = frame.value const iframe = frame.value
if (!patch.url || !iframe) return if (!url || !iframe) return
const tab = await chrome.tabs.getCurrent() const tab = await chrome.tabs.getCurrent()
await updateFrameNetRules({ await updateFrameNetRules({
ua: patch.ua, ua: ua,
tabIds: [tab?.id || -1], tabIds: [tab?.id || -1],
initiatorDomains: [chrome.runtime.id],
}) })
await chrome.scripting.updateContentScripts([ await chrome.scripting.updateContentScripts([
...@@ -90,7 +86,7 @@ watch(patch, async (patch) => { ...@@ -90,7 +86,7 @@ watch(patch, async (patch) => {
}, },
]) ])
frameUrl.value = patch.url frameUrl.value = url
pageInfo.url = "" pageInfo.url = ""
pageInfo.title = "" pageInfo.title = ""
pageInfo.icon = "" pageInfo.icon = ""
...@@ -108,12 +104,19 @@ watch(patch, async (patch) => { ...@@ -108,12 +104,19 @@ watch(patch, async (patch) => {
}) })
} catch (e) { } catch (e) {
console.warn(e) console.warn(e)
const url = new URL(props.url) let loadUrl = ""
let loadUrl = url.origin + patch.l if (props.preloadUrl) {
if (!loadUrl) { const u = new URL(props.preloadUrl || "", props.url)
const u = await findFrameLoadUrl(loadUrls) loadUrl = u.href
}
if (!loadUrl && props.preloadCandidates) {
const u = await findFrameLoadUrl(props.preloadCandidates, props.url)
u && (loadUrl = u) u && (loadUrl = u)
} }
if (!loadUrl) {
console.warn("No preload url found")
return
}
try { try {
frameUrl.value = loadUrl frameUrl.value = loadUrl
...@@ -148,7 +151,7 @@ watch(patch, async (patch) => { ...@@ -148,7 +151,7 @@ watch(patch, async (patch) => {
}, },
"*" "*"
) )
}) }
function handleFrameMessage(e: MessageEvent) { function handleFrameMessage(e: MessageEvent) {
console.log("frame message: ", e, e.source !== frame.value?.contentWindow) console.log("frame message: ", e, e.source !== frame.value?.contentWindow)
...@@ -164,47 +167,56 @@ function handleFrameMessage(e: MessageEvent) { ...@@ -164,47 +167,56 @@ function handleFrameMessage(e: MessageEvent) {
pageInfo.title = e.data.title pageInfo.title = e.data.title
pageInfo.icon = e.data.icon pageInfo.icon = e.data.icon
emit("pageInfo", pageInfo) emit("load", pageInfo)
} }
break break
case FrameMessageType.invokeResponse:
webviewInvoke.value?.handleResMsg(e.data)
break
} }
} }
function reload() { function reload() {
console.log("reload") console.log("reload", pageInfo.url)
frame.value?.contentWindow?.postMessage( webviewInvoke.value
{ ?.invoke({
type: FrameMessageType.reload, 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() { function goBack() {
console.log("goBack") console.log("goBack")
webviewInvoke.value?.invoke({
frame.value?.contentWindow?.postMessage( func: WebviewFunc.goBack,
{ args: [],
type: FrameMessageType.goBack, })
},
"*"
)
} }
function goForward() { function goForward() {
console.log("goForward") console.log("goForward")
webviewInvoke.value?.invoke({
frame.value?.contentWindow?.postMessage( func: WebviewFunc.goForward,
{ args: [],
type: FrameMessageType.goForward, })
},
"*"
)
} }
</script> </script>
<template> <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> </template>
<style scoped></style> <style scoped></style>
...@@ -9,7 +9,7 @@ import { useI18n } from "@/utils/i18n" ...@@ -9,7 +9,7 @@ import { useI18n } from "@/utils/i18n"
import { getDocItem, devConfig, type SiteConfig } from "./helper" import { getDocItem, devConfig, type SiteConfig } from "./helper"
import config from "@/assets/config.json" import config from "@/assets/config.json"
import { getLocal } from "@/utils/ext" import { getLocal } from "@/utils/ext"
import { chatDocPrompt } from "@/utils/prompt" import { chatDocPrompt } from "@/utils/const"
import { autoPointerCapture } from "@/utils/dom" import { autoPointerCapture } from "@/utils/dom"
const { t } = useI18n() const { t } = useI18n()
...@@ -211,4 +211,4 @@ onUnmounted(() => { ...@@ -211,4 +211,4 @@ onUnmounted(() => {
</div> </div>
</template> </template>
<style scoped></style> <style scoped></style>@/utils/const
\ No newline at end of file \ No newline at end of file
<script setup lang="ts"> <script setup lang="ts">
import { watch, computed, reactive, ref, onMounted, watchEffect } from "vue" import { watch, computed, reactive, ref, onMounted, watchEffect } from "vue"
import { chatDocsPanel } from "@/store/content" import { chatDocsPanel } from "@/store/content"
import { contentService } from "@/utils/service" import { contentInvoke } from "@/utils/invoke"
import { convertBlobToBase64, semanticClip } from "@/utils/utils" import { convertBlobToBase64, semanticClip } from "@/utils/utils"
import ScrollView from "@/components/ScrollView.vue" import ScrollView from "@/components/ScrollView.vue"
import IconArrowBack from "@/components/icons/IconArrowBack.vue" import IconArrowBack from "@/components/icons/IconArrowBack.vue"
...@@ -21,7 +21,7 @@ import { ...@@ -21,7 +21,7 @@ import {
getInputValue, getInputValue,
} from "@/utils/dom" } from "@/utils/dom"
import { getLocal } from "@/utils/ext" import { getLocal } from "@/utils/ext"
import { chatDocPrompt } from "@/utils/prompt" import { chatDocPrompt } from "@/utils/const"
import { useI18n } from "@/utils/i18n" import { useI18n } from "@/utils/i18n"
type Sheet = "" | "docSelect" | "promptTemplate" type Sheet = "" | "docSelect" | "promptTemplate"
...@@ -126,7 +126,7 @@ watch( ...@@ -126,7 +126,7 @@ watch(
if (maxInputType !== "token") return if (maxInputType !== "token") return
if (!message) return if (!message) return
const tokenLength = await contentService.calcTokens(message) const tokenLength = await contentInvoke.calcTokens(message)
const rate = (message.length / tokenLength) * 0.95 const rate = (message.length / tokenLength) * 0.95
const exceedMaxInput = tokenLength > maxInput const exceedMaxInput = tokenLength > maxInput
if (exceedMaxInput) { if (exceedMaxInput) {
...@@ -172,7 +172,7 @@ watch( ...@@ -172,7 +172,7 @@ watch(
if (item.kind == "file" && typeof item.data != "string") { if (item.kind == "file" && typeof item.data != "string") {
const url = await convertBlobToBase64(item.data) const url = await convertBlobToBase64(item.data)
const results = await contentService.parseDoc({ const results = await contentInvoke.parseDoc({
key: item.key, key: item.key,
type: item.type, type: item.type,
size: item.data.size, size: item.data.size,
...@@ -640,4 +640,4 @@ input:hover { ...@@ -640,4 +640,4 @@ input:hover {
transform: translate(100%, 0); transform: translate(100%, 0);
} }
} }
</style> </style>@/utils/const@/utils/invoke/service@/utils/invokeb
\ No newline at end of file \ 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" ...@@ -5,7 +5,7 @@ import { getLocal } from "@/utils/ext"
import IconClose from "@/components/icons/IconClose.vue" import IconClose from "@/components/icons/IconClose.vue"
import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue" import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue"
import PageScrollbar from "./PageScrollbar.vue" import PageScrollbar from "./PageScrollbar.vue"
import { MessageType } from "@/types" import { FrameMessageType, MessageType } from "@/types"
import { autoPointerCapture } from "@/utils/dom" import { autoPointerCapture } from "@/utils/dom"
const props = defineProps<{ const props = defineProps<{
...@@ -16,6 +16,7 @@ const emit = defineEmits(["close", "hide"]) ...@@ -16,6 +16,7 @@ const emit = defineEmits(["close", "hide"])
const sidebarHtml = chrome.runtime.getURL("sidebar.html") const sidebarHtml = chrome.runtime.getURL("sidebar.html")
const sidebarUrl = sidebarHtml + "?mode=content" const sidebarUrl = sidebarHtml + "?mode=content"
const logoUrl = chrome.runtime.getURL("/logo.svg") const logoUrl = chrome.runtime.getURL("/logo.svg")
const extRootUrl = chrome.runtime.getURL("/")
let sheet: CSSStyleSheet | null = null let sheet: CSSStyleSheet | null = null
let invisibleTimer = 0 let invisibleTimer = 0
...@@ -62,6 +63,21 @@ function handleKeyUp(e: KeyboardEvent) { ...@@ -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( watch(
width, width,
debounce((value) => { debounce((value) => {
...@@ -90,6 +106,7 @@ onMounted(() => { ...@@ -90,6 +106,7 @@ onMounted(() => {
updatePageStyle(width.value) updatePageStyle(width.value)
window.addEventListener("keydown", handleKeydown) window.addEventListener("keydown", handleKeydown)
window.addEventListener("keyup", handleKeyUp) window.addEventListener("keyup", handleKeyUp)
window.addEventListener("message", handleMessage)
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
type: MessageType.registerContentSidebar, type: MessageType.registerContentSidebar,
}) })
...@@ -99,6 +116,7 @@ onUnmounted(() => { ...@@ -99,6 +116,7 @@ onUnmounted(() => {
disablePageStyle() disablePageStyle()
window.removeEventListener("keydown", handleKeydown) window.removeEventListener("keydown", handleKeydown)
window.removeEventListener("keyup", handleKeyUp) window.removeEventListener("keyup", handleKeyUp)
window.removeEventListener("message", handleMessage)
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
type: MessageType.unregisterContentSidebar, type: MessageType.unregisterContentSidebar,
}) })
...@@ -121,7 +139,10 @@ onUnmounted(() => { ...@@ -121,7 +139,10 @@ onUnmounted(() => {
@pointerdown="autoPointerCapture" @pointerdown="autoPointerCapture"
@pointermove="pointermove" @pointermove="pointermove"
></div> ></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 <button
@click="" @click=""
class="size-7 flex items-center justify-center mr-auto" class="size-7 flex items-center justify-center mr-auto"
......
...@@ -31,9 +31,10 @@ const emit = defineEmits({ ...@@ -31,9 +31,10 @@ const emit = defineEmits({
<div class="flex flex-wrap gap-x-4 gap-y-4 justify-center"> <div class="flex flex-wrap gap-x-4 gap-y-4 justify-center">
<SiteButton <SiteButton
v-for="item of recentItems" v-for="item of recentItems.slice(0, 12)"
:icon="item.icon" :icon="item.icon"
:title="item.title" :title="item.title"
:url="item.url"
badge="remove" badge="remove"
@click="emit('go', item.url)" @click="emit('go', item.url)"
@remove="emit('removeRecentItem', item.url)" @remove="emit('removeRecentItem', item.url)"
......
...@@ -6,15 +6,20 @@ import { ...@@ -6,15 +6,20 @@ import {
pipWindow, pipWindow,
sidebarAddon, sidebarAddon,
} from "@/store/content" } from "@/store/content"
import { ContentEventType, FrameMessageType, MessageType } from "@/types" import {
ContentEventType,
FrameMessageType,
MessageType,
WebviewFunc,
WindowName,
} from "@/types"
import Copilot from "./Copilot.vue" import Copilot from "./Copilot.vue"
import { waitMessage } from "@/utils/ext"
import { import {
dispatchContentEvent, dispatchContentEvent,
addContentEventListener, addContentEventListener,
removeContentEventListener, removeContentEventListener,
} from "@/content/event" } from "@/content/event"
import { contentService } from "@/utils/service" import { contentInvoke } from "@/utils/invoke"
import { getPageIcon } from "@/utils/dom" import { getPageIcon } from "@/utils/dom"
// import { PipEventName } from "@/types/pip" // import { PipEventName } from "@/types/pip"
...@@ -47,7 +52,7 @@ function handleMessage( ...@@ -47,7 +52,7 @@ function handleMessage(
}) })
break break
case MessageType.invokeResponse: case MessageType.invokeResponse:
contentService.handleMessage(message) contentInvoke.handleResMsg(message)
break break
case MessageType.showChatDocs: case MessageType.showChatDocs:
chatDocsPanel.visible = true chatDocsPanel.visible = true
...@@ -150,18 +155,50 @@ function handleFrameMessage(e: MessageEvent) { ...@@ -150,18 +155,50 @@ function handleFrameMessage(e: MessageEvent) {
type: ContentEventType.escapeLoad, type: ContentEventType.escapeLoad,
detail: { url: e.data.url }, detail: { url: e.data.url },
}) })
case FrameMessageType.reload: case FrameMessageType.invokeRequest:
return location.reload() handleInvokeRequest(e.data, e.source as Window)
case FrameMessageType.goBack: break
return history.back() }
case FrameMessageType.goForward: }
return history.forward()
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() run()
} else { }
// webview
if (window.top !== window && window.name.startsWith(WindowName.webview)) {
window.addEventListener("message", handleFrameMessage) window.addEventListener("message", handleFrameMessage)
window.parent?.postMessage( window.parent?.postMessage(
{ {
......
import { ContentEventType } from "@/types" import { ContentEventType, WindowName } from "@/types"
import { addContentEventListener } from "./event" import { addContentEventListener } from "./event"
import { copilotNavigateTo, pip, fetchDoc, writeHtml } from "./pip" import { copilotNavigateTo, pip, fetchDoc, writeHtml } from "./pip"
...@@ -27,3 +27,8 @@ async function handleEscapeLoadEvent(event: CustomEvent | Event) { ...@@ -27,3 +27,8 @@ async function handleEscapeLoadEvent(event: CustomEvent | Event) {
addContentEventListener(ContentEventType.pip, handlePipEvent) addContentEventListener(ContentEventType.pip, handlePipEvent)
addContentEventListener(ContentEventType.pipLoad, handlePipLoadDocEvent) addContentEventListener(ContentEventType.pipLoad, handlePipLoadDocEvent)
addContentEventListener(ContentEventType.escapeLoad, handleEscapeLoadEvent) 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" ...@@ -21,6 +21,7 @@ import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue"
import SiteButton from "@/components/SiteButton.vue" import SiteButton from "@/components/SiteButton.vue"
import { getIsEdge } from "@/utils/ext" import { getIsEdge } from "@/utils/ext"
import { handleImgError } from "@/utils/dom" import { handleImgError } from "@/utils/dom"
import { homeUrl, feedbackUrl } from "@/utils/const"
const globeImg = chrome.runtime.getURL("img/globe.svg") const globeImg = chrome.runtime.getURL("img/globe.svg")
...@@ -151,7 +152,7 @@ async function openSidebar(url = "") { ...@@ -151,7 +152,7 @@ async function openSidebar(url = "") {
}) })
const win = await chrome.windows.getCurrent() const win = await chrome.windows.getCurrent()
await chrome.storage.session.set({ await chrome.storage.session.set({
sidebarUrls: { sidepanel: url }, sidebarInitUrl: { sidepanel: url },
}) })
await chrome.sidePanel.open({ windowId: win.id! }) await chrome.sidePanel.open({ windowId: win.id! })
...@@ -160,7 +161,7 @@ async function openSidebar(url = "") { ...@@ -160,7 +161,7 @@ async function openSidebar(url = "") {
async function openContentSidebar(url = "") { async function openContentSidebar(url = "") {
await chrome.storage.session.set({ await chrome.storage.session.set({
sidebarUrls: { content: url }, sidebarInitUrl: { content: url },
}) })
await chrome.tabs.sendMessage(activeTab.value.id!, { await chrome.tabs.sendMessage(activeTab.value.id!, {
type: MessageType.openContentSidebar, type: MessageType.openContentSidebar,
...@@ -180,7 +181,7 @@ async function handleClickLaunch(url: string) { ...@@ -180,7 +181,7 @@ async function handleClickLaunch(url: string) {
} }
function feedback() { function feedback() {
open("https://tawk.to/anythingcopilot", "_blank") open(feedbackUrl, "_blank")
} }
function fivestar() { function fivestar() {
open( open(
...@@ -194,7 +195,7 @@ function fivestar() { ...@@ -194,7 +195,7 @@ function fivestar() {
} }
function goHome() { function goHome() {
open("https://ziziyi.com/copilot", "_blank") open(homeUrl, "_blank")
} }
function showChatDocs() { function showChatDocs() {
...@@ -347,6 +348,7 @@ function showChatDocs() { ...@@ -347,6 +348,7 @@ function showChatDocs() {
:icon="item.icon" :icon="item.icon"
:title="item.title" :title="item.title"
:badge="keyboard.shift || (isEdge && !avaiable) ? 'popup' : 'sidebar'" :badge="keyboard.shift || (isEdge && !avaiable) ? 'popup' : 'sidebar'"
:url="item.url"
small small
@click=" @click="
() => { () => {
......
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, reactive, computed, onUnmounted } from "vue" import { ref, onMounted, reactive, computed, onUnmounted, watch } from "vue"
import { getLocal, getSession, isProtectedUrl } from "@/utils/ext" import {
getLocal,
getSession,
isProtectedUrl,
updateFrameNetRules,
updateUANetRules,
} from "@/utils/ext"
import config from "@/assets/config.json" 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 { useI18n } from "@/utils/i18n"
import { MessageType } from "@/types" import { MessageType } from "@/types"
import SidebarHome from "@/components/sidebar/SidebarHome.vue" import SidebarHome from "@/components/sidebar/SidebarHome.vue"
...@@ -12,22 +18,138 @@ import IconNavigateBefore from "@/components/icons/IconNavigateBefore.vue" ...@@ -12,22 +18,138 @@ import IconNavigateBefore from "@/components/icons/IconNavigateBefore.vue"
import IconNavigateNext from "@/components/icons/IconNavigateNext.vue" import IconNavigateNext from "@/components/icons/IconNavigateNext.vue"
import IconRefresh from "@/components/icons/IconRefresh.vue" import IconRefresh from "@/components/icons/IconRefresh.vue"
import IconHome from "@/components/icons/IconHome.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 logoUrl = chrome.runtime.getURL("/logo.svg")
const globeImg = chrome.runtime.getURL("img/globe.svg")
const { t } = useI18n() 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 mode = ref("")
const popularItems = reactive(config.data.popularSites) const popularItems = reactive(config.data.popularSites)
const recentItems = reactive<{ url: string; title: string; icon: string }[]>([]) const recentItems = reactive<{ url: string; title: string; icon: string }[]>([])
const currentTab = reactive({ const currentTab = reactive({
tabId: 0, tabId: -1,
}) })
const ua = ref("") const defaultUA = ref(config.data.embedView.defaultUA)
const webview = ref<InstanceType<typeof Webview> | null>(null) const patchs = reactive(config.data.webviewPatchs)
const preloadUrls = reactive(config.data.loadCandidates)
const protectedUrl = computed(() => { const pages = reactive<{ url: string }[]>([])
return isProtectedUrl(url.value) 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) { async function handleMessage(message: any) {
...@@ -38,30 +160,25 @@ async function handleMessage(message: any) { ...@@ -38,30 +160,25 @@ async function handleMessage(message: any) {
currentTab.tabId = current?.id || -1 currentTab.tabId = current?.id || -1
} }
if (message.tabId == currentTab.tabId) { if (message.tabId == currentTab.tabId) {
url.value = message.url const url = message.url
go(url)
} }
break break
} }
} }
async function updateRecentItems(pageInfo: { async function handlePageLoad(i: number, pageInfo: PageInfo) {
url: string pagesInfo[i] = pageInfo
title: string
icon: string
}) {
if (mode.value === "content") { if (mode.value === "content") {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
type: MessageType.registerContentSidebar, type: MessageType.registerContentSidebar,
url: url.value, pages: pages,
}) })
} }
const { sidebarRecentItems } = await getLocal({ const { sidebarRecentItems } = await getLocal({
sidebarRecentItems: [] as { sidebarRecentItems: [] as PageInfo[],
url: string
icon: string
title: string
}[],
}) })
const index = sidebarRecentItems.findIndex((i) => i.url === pageInfo.url) const index = sidebarRecentItems.findIndex((i) => i.url === pageInfo.url)
...@@ -69,7 +186,7 @@ async function updateRecentItems(pageInfo: { ...@@ -69,7 +186,7 @@ async function updateRecentItems(pageInfo: {
sidebarRecentItems.splice(index, 1) sidebarRecentItems.splice(index, 1)
} }
sidebarRecentItems.unshift(pageInfo) sidebarRecentItems.unshift(pageInfo)
sidebarRecentItems.splice(12) sidebarRecentItems.splice(36)
recentItems.splice(0, recentItems.length, ...sidebarRecentItems) recentItems.splice(0, recentItems.length, ...sidebarRecentItems)
await chrome.storage.local.set({ sidebarRecentItems }) await chrome.storage.local.set({ sidebarRecentItems })
...@@ -92,105 +209,258 @@ async function removeRecentItem(url: string) { ...@@ -92,105 +209,258 @@ async function removeRecentItem(url: string) {
await chrome.storage.local.set({ sidebarRecentItems }) await chrome.storage.local.set({ sidebarRecentItems })
} }
onMounted(() => { function go(link: string) {
const q = new URLSearchParams(location.search) const len = pages.push({ url: link })
mode.value = q.get("mode") || "sidepanel" active.value = len - 1
// 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({ function goBack() {
popularSites: config.data.popularSites, const webview = webviews.value[active.value]
sidebarRecentItems: [], webview?.goBack()
}).then(({ popularSites, sidebarRecentItems }) => { }
if (popularSites) {
popularItems.splice(0, popularItems.length, ...popularSites) function goForward() {
} const webview = webviews.value[active.value]
if (sidebarRecentItems) { webview?.goForward()
recentItems.splice(0, recentItems.length, ...sidebarRecentItems) }
}
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({ const mobileHosts = ["mobile.ziziyi.com"]
sidebarUrls: {} as Record<string, string>, const desktopHosts = ["desktop.ziziyi.com"]
}).then(({ sidebarUrls }) => { Object.entries(hostUA).forEach(([k, v]) => {
console.log("[sidebar]", sidebarUrls, mode.value) if (v === 1) {
if (sidebarUrls && sidebarUrls[mode.value]) { mobileHosts.push(k)
url.value = sidebarUrls[mode.value] } else {
desktopHosts.push(k)
} }
}) })
chrome.tabs.getCurrent().then((t) => { await updateUANetRules({
currentTab.tabId = t?.id || -1 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(() => { function handlePointerEnter() {
chrome.runtime.onMessage.removeListener(handleMessage) isPointerIn.value = true
}) clearTimeout(timer)
}
function go(link: string) { function handlePointerLeave() {
url.value = link timer = window.setTimeout(() => {
isPointerIn.value = false
}, 350)
} }
</script> </script>
<template> <template>
<div class="w-full h-screen flex flex-col"> <div
class="w-full h-screen flex flex-col"
@pointerenter="handlePointerEnter"
@pointerleave="handlePointerLeave"
>
<div <div
:class="[ :class="[
'flex gap-1 items-center justify-between h-8 px-1', 'flex gap-1 items-center justify-between h-9 px-1 z-10 shadow-sm',
'*:size-7 *:rounded-full *:flex *:items-center *:justify-center ', '*:size-7 *:flex *:items-center *:justify-center ',
]" ]"
> >
<button @click="" class="group hover:bg-background-soft mr-auto"> <a
<!-- <img class="size-4" :src="logoUrl" /> --> @click="(e) => (e.preventDefault(), (active = -1))"
<IconHome class="size-5 group-active:scale-90 transition-transform" /> :href="homeUrl"
</button> :class="[
<button class="group hover:bg-background-soft" @click="webview?.goBack()"> '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 <IconNavigateBefore
class="size-5 scale-125 group-active:-translate-x-1 transition-transform" class="size-5 scale-125 group-active:-translate-x-1 transition-transform"
/> />
</button> </button>
<button <button
class="group hover:bg-background-soft" @click="goForward()"
@click="webview?.goForward()" class="group hover:bg-background-soft rounded-full"
> >
<IconNavigateNext <IconNavigateNext
class="size-5 scale-125 group-active:translate-x-1 transition-transform" class="size-5 scale-125 group-active:translate-x-1 transition-transform"
/> />
</button> </button> -->
<button class="group hover:bg-background-soft" @click="webview?.reload()"> <button
@click="reload()"
class="group hover:bg-background-soft rounded-full"
>
<IconRefresh <IconRefresh
class="size-5 group-active:rotate-180 transition-transform" class="size-5 group-active:rotate-180 transition-transform"
/> />
</button> </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 <IconSplitscreenRight
class="size-5 scale-95 group-active:scale-90 transition-transform" class="size-5 scale-95 group-active:scale-90 transition-transform"
/> />
</button> </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" /> <IconClose class="size-5 group-active:scale-90 transition-transform" />
</button> </button>
</div> </div>
<div class="w-full h-full" v-if="!protectedUrl"> <div :class="['w-full h-full', { hidden: active != -1 }]">
<Webview <SidebarHome
ref="webview" :recentItems="recentItems"
:url="url" :popularItems="popularItems"
:ua="ua" @go="go"
@page-info="updateRecentItems" @remove-recent-item="removeRecentItem"
/> />
</div> </div>
<SidebarHome <div
v-else v-for="(attr, i) of webviewsAttr"
:recentItems="recentItems" :class="[
:popularItems="popularItems" 'w-full h-full rounded-sm overflow-hidden',
@go="go" { hidden: active != i },
@remove-recent-item="removeRecentItem" ]"
/> >
<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> </div>
<!-- <LoadingBar /> --> <!-- <LoadingBar /> -->
......
...@@ -54,7 +54,18 @@ export enum FrameMessageType { ...@@ -54,7 +54,18 @@ export enum FrameMessageType {
webviewRun = "anything-copilot_webview-run", webviewRun = "anything-copilot_webview-run",
escapeLoad = "anything-copilot_escape-load", escapeLoad = "anything-copilot_escape-load",
pageInfo = "anything-copilot_page-info", pageInfo = "anything-copilot_page-info",
reload = "anything-copilot_reload", collapseSidebar = "anything-copilot_collapse-sidebar",
goBack = "anything-copilot_go-back", closeSidebar = "anything-copilot_close-sidebar",
goForward = "anything-copilot_go-forward", 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 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( ...@@ -217,7 +217,7 @@ export async function waitFor(
} }
export function getPageIcon() { export function getPageIcon() {
const icons = document.querySelectorAll<HTMLLinkElement>('link[rel="icon"]') const icons = document.querySelectorAll<HTMLLinkElement>('link[rel~="icon"]')
for (let item of icons) { for (let item of icons) {
if (item.getAttribute("type") == "image/svg+xml") { if (item.getAttribute("type") == "image/svg+xml") {
return item.href return item.href
...@@ -231,6 +231,13 @@ export function getPageIcon() { ...@@ -231,6 +231,13 @@ export function getPageIcon() {
return icons[0].href return icons[0].href
} }
const touchIcon = document.querySelector<HTMLLinkElement>(
'link[rel="apple-touch-icon"]'
)
if (touchIcon) {
return touchIcon.href
}
return location.origin + "/favicon.ico" return location.origin + "/favicon.ico"
} }
......
...@@ -185,56 +185,116 @@ export function getSession<T extends Record<string, any>>(key: string | T) { ...@@ -185,56 +185,116 @@ export function getSession<T extends Record<string, any>>(key: string | T) {
} }
type NetRulesOptions = { type NetRulesOptions = {
id?: number
ua?: string ua?: string
tabIds?: number[] tabIds?: number[]
requestDomains?: string[]
initiatorDomains?: string[] initiatorDomains?: string[]
enabled?: boolean
} }
export async function updateFrameNetRules( export async function updateFrameNetRules({
{ ua, tabIds, initiatorDomains }: NetRulesOptions = { id,
tabIds: [-1], ua,
initiatorDomains: [chrome.runtime.id], tabIds,
} initiatorDomains,
) { requestDomains,
enabled,
}: NetRulesOptions) {
ua = ua || navigator.userAgent ua = ua || navigator.userAgent
// const noneTab = tabIds?.length == 1 && tabIds[0] == -1
// if (!initiatorDomains && !noneTab) {
// initiatorDomains = [chrome.runtime.id]
// }
await chrome.declarativeNetRequest.updateSessionRules({ await chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [1], removeRuleIds: [id || 1],
addRules: [ addRules:
{ enabled != false
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", id: id || 1,
operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE, priority: 1,
}, action: {
], type: chrome.declarativeNetRequest.RuleActionType
requestHeaders: [ .MODIFY_HEADERS,
{ responseHeaders: [
header: "User-Agent", {
value: ua, header: "X-Frame-Options",
operation: chrome.declarativeNetRequest.HeaderOperation.SET, 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", id,
value: "document", priority: 2,
operation: chrome.declarativeNetRequest.HeaderOperation.SET, 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) { ...@@ -250,12 +310,13 @@ export function isProtectedUrl(url: string) {
} }
const isEdge = getIsEdge() const isEdge = getIsEdge()
if (isEdge && u.hostname == "microsoftedge.microsoft.com") { if (isEdge && u.hostname == "microsoftedge.microsoft.com") {
return true return true
} }
if (u.hostname == "chrome.google.com") { if (
["chromewebstore.google.com", "chrome.google.com"].includes(u.hostname)
) {
return true return true
} }
......
import { MessageType, ServiceFunc, type ParseDocOptions } from "@/types" import { MessageType, ServiceFunc, type ParseDocOptions } from "@/types"
import Invoke from "./Invoke" import Invoke from "./Invoke"
class Service extends Invoke { class ContentInvoke extends Invoke {
public async send(req: any) { public async send(req: any) {
const key = this.key const key = this.key
const response = await chrome.runtime.sendMessage({ const response = await chrome.runtime.sendMessage({
...@@ -13,7 +13,7 @@ class Service extends Invoke { ...@@ -13,7 +13,7 @@ class Service extends Invoke {
return { key, response } return { key, response }
} }
public handleMessage(message: any) { public handleResMsg(message: any) {
if (message?.type === MessageType.invokeResponse) { if (message?.type === MessageType.invokeResponse) {
const { key, success, payload } = message const { key, success, payload } = message
this.setReturnValue(key, success, payload) this.setReturnValue(key, success, payload)
...@@ -42,6 +42,6 @@ class Service extends Invoke { ...@@ -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 = { ...@@ -6,6 +6,13 @@ type CallbackItem = {
interface InvokeReq { interface InvokeReq {
func: string func: string
args: any[] args: any[]
timeout?: number
}
interface InvokeRes {
key: string
success: boolean
value: any
} }
abstract class Invoke { abstract class Invoke {
...@@ -25,14 +32,14 @@ abstract class Invoke { ...@@ -25,14 +32,14 @@ abstract class Invoke {
return `${this.name}-${this.uniqueId}-${this.count++}` return `${this.name}-${this.uniqueId}-${this.count++}`
} }
protected getReturnValue(key: string, req: InvokeReq, timeout?: number) { protected getReturnValue(key: string, req: InvokeReq) {
let timer: any let timer: any
const { func, timeout } = req
const promise = new Promise<any>((resolve, reject) => { const promise = new Promise<any>((resolve, reject) => {
this.pendingCallback[key] = { resolve, reject } this.pendingCallback[key] = { resolve, reject }
if (timeout) { if (timeout) {
timer = setTimeout( timer = setTimeout(
() => () => reject(`"${this.name}" invoke timeout: ${func} key: ${key}`),
reject(`"${this.name}" invoke timeout: ${req.func} key: ${key}`),
timeout timeout
) )
} }
...@@ -46,11 +53,11 @@ abstract class Invoke { ...@@ -46,11 +53,11 @@ abstract class Invoke {
return promise return promise
} }
protected setReturnValue(key: string, success: boolean, payload: any) { protected setReturnValue(key: string, success: boolean, value: any) {
const callback = this.pendingCallback[key] const callback = this.pendingCallback[key]
if (callback) { if (callback) {
const fn = success != false ? callback.resolve : callback.reject const fn = success != false ? callback.resolve : callback.reject
fn(payload) fn(value)
} else { } else {
console.error(`unknown invoke callback message: ${key}`) console.error(`unknown invoke callback message: ${key}`)
console.log(this.pendingCallback) console.log(this.pendingCallback)
...@@ -58,11 +65,12 @@ abstract class Invoke { ...@@ -58,11 +65,12 @@ abstract class Invoke {
} }
abstract send(req: InvokeReq): Promise<{ key: string; response: any }> 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) 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) => { ...@@ -52,7 +52,10 @@ export const semanticClip = (text: string, maxLength: number) => {
} }
/** Find URL from the same origin that can be embedded */ /** 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() const abortController = new AbortController()
let resolve: null | ((url: string) => void) = null let resolve: null | ((url: string) => void) = null
...@@ -65,8 +68,9 @@ export async function findFrameLoadUrl(urls: string[]): Promise<string> { ...@@ -65,8 +68,9 @@ export async function findFrameLoadUrl(urls: string[]): Promise<string> {
return !csp.includes("frame-ancestors") return !csp.includes("frame-ancestors")
} }
const promises = urls.map((url) => const promises = urls.map((url) => {
fetch(url, { const u = new URL(url, base)
return fetch(u.href, {
signal: abortController.signal, signal: abortController.signal,
}) })
.then((res) => { .then((res) => {
...@@ -74,12 +78,12 @@ export async function findFrameLoadUrl(urls: string[]): Promise<string> { ...@@ -74,12 +78,12 @@ export async function findFrameLoadUrl(urls: string[]): Promise<string> {
const xFrameOptions = h.get("X-Frame-Options") const xFrameOptions = h.get("X-Frame-Options")
const csp = h.get("Content-Security-Policy") const csp = h.get("Content-Security-Policy")
if (!xFrameOptions && checkCSP(csp)) { if (!xFrameOptions && checkCSP(csp)) {
resolve && resolve(url) resolve && resolve(u.href)
abortController.abort() abortController.abort()
} }
}) })
.catch(() => {}) .catch(() => {})
) })
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
resolve && resolve("") 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