Commit 1d61640e authored by Domi's avatar Domi

feat: chrome sidebar

parent dd609683
...@@ -13,6 +13,7 @@ dist ...@@ -13,6 +13,7 @@ dist
dist-ssr dist-ssr
coverage coverage
*.local *.local
public/js
/cypress/videos/ /cypress/videos/
/cypress/screenshots/ /cypress/screenshots/
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/@vite/client"></script> <script type="module" src="./src/pages/dev.ts"></script>
<script type="module" src="/src/pages/dev.ts"></script>
</body> </body>
</html> </html>
import * as esbuild from "esbuild" import * as esbuild from "esbuild"
const isWatch = process.argv.includes("--watch") import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"
// const isWatch = process.argv.includes("--watch")
// const isToPublic = process.argv.includes("--public")
const isWatch = process.env.NODE_ENV === "development"
const workingDir = resolve(dirname(fileURLToPath(import.meta.url)))
const ctx = await esbuild.context({ const ctx = await esbuild.context({
absWorkingDir: resolve(dirname(fileURLToPath(import.meta.url))),
entryPoints: { entryPoints: {
bg: "./src/bg/index.ts", // bg: "./src/bg/index.ts",
"js/content-main": "./src/content/main.ts", "js/content-main": "./src/content/main.ts",
"js/content-frame": "./src/content/frame.ts",
"js/pdf.worker": "./src/assets/pdf.worker.js", "js/pdf.worker": "./src/assets/pdf.worker.js",
}, },
bundle: true, bundle: true,
format: "iife", format: "iife",
outdir: "./dist", outdir: "./public",
alias: { alias: {
"@": "./src/", "@": "./src/",
}, },
......
...@@ -8,6 +8,6 @@ ...@@ -8,6 +8,6 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="./guide.ts"></script> <script type="module" src="./src/pages/guide.ts"></script>
</body> </body>
</html> </html>
...@@ -8,6 +8,6 @@ ...@@ -8,6 +8,6 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="./offscreen.ts"></script> <script type="module" src="./src/pages/offscreen.ts"></script>
</body> </body>
</html> </html>
This diff is collapsed.
{ {
"name": "picture-in-picture", "name": "anything-copilot",
"version": "0.0.0", "version": "1.0.0",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"dev": "run-p dev:page dev:content dev:js", "dev:vite": "vite",
"dev:page": "cross-env NODE_ENV=development vite build --watch", "dev": "run-p dev:vite build:esbuild",
"dev:content": "vite build --watch -c vite.content.config.ts", "build": "npm run build:esbuild && vite build",
"dev:js": "node esbuild.mjs --watch", "build:esbuild": "node esbuild.mjs",
"start": "vite -c vite.config.ts --port 3000",
"build": "run-p type-check build:js build:content \"build-only {@}\" --",
"build-only": "vite build",
"build:content": "vite build -c vite.content.config.ts",
"build:js": "node esbuild.mjs",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false", "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"zip:win": "7z a anything-copilot.zip .\\dist\\*", "zip:win": "7z a anything-copilot.zip .\\dist\\*",
"zip": "", "zip": "",
...@@ -36,9 +32,10 @@ ...@@ -36,9 +32,10 @@
"vue-i18n": "^9.7.0" "vue-i18n": "^9.7.0"
}, },
"devDependencies": { "devDependencies": {
"@crxjs/vite-plugin": "^2.0.0-beta.23",
"@intlify/unplugin-vue-i18n": "^1.5.0", "@intlify/unplugin-vue-i18n": "^1.5.0",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@types/chrome": "^0.0.251", "@types/chrome": "^0.0.261",
"@types/lodash-es": "^4.17.11", "@types/lodash-es": "^4.17.11",
"@types/node": "^18.18.5", "@types/node": "^18.18.5",
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^4.4.0",
...@@ -48,6 +45,8 @@ ...@@ -48,6 +45,8 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"npm-run-all2": "^6.1.1", "npm-run-all2": "^6.1.1",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"rollup-plugin-copy": "^3.5.0",
"sass": "^1.71.1",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "~5.2.0", "typescript": "~5.2.0",
"vite": "^4.4.11", "vite": "^4.4.11",
......
...@@ -8,6 +8,6 @@ ...@@ -8,6 +8,6 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="./popup.ts"></script> <script type="module" src="./src/pages/popup.ts"></script>
</body> </body>
</html> </html>
module.exports = { export default {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anything Copilot</title>
</head>
<body>
<script type="module" src="./src/pages/sidebar.ts"></script>
<div id="app">
</div>
</body>
</html>
...@@ -72,6 +72,36 @@ ...@@ -72,6 +72,36 @@
"wait": "div > span:has(svg[width=\"240\"]):not([style*=\"display: none\"])" "wait": "div > span:has(svg[width=\"240\"]):not([style*=\"display: none\"])"
} }
} }
],
"webviewPatchs": [
{
"re": "www.google.com",
"l": "",
"ua": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36"
}
],
"loadCandidates": ["/robots.txt", "/sitemap.xml", "/logo.svg"],
"popularSites": [
{
"url": "https://chat.openai.com/",
"title": "ChatGPT",
"icon": "https://r2.ziziyi.com/copilot/chatgpt.svg"
},
{
"url": "https://copilot.microsoft.com/",
"title": "Microsoft Copilot",
"icon": "https://r2.ziziyi.com/copilot/ms-copilot.svg"
},
{
"url": "https://gemini.google.com/",
"title": "Gemini",
"icon": "https://r2.ziziyi.com/copilot/gemini.svg"
},
{
"url": "https://claude.ai/",
"title": "Claude",
"icon": "https://r2.ziziyi.com/copilot/claude-ai.svg"
}
] ]
} }
} }
...@@ -20,18 +20,18 @@ a, ...@@ -20,18 +20,18 @@ a,
} }
} }
@media (min-width: 1024px) { /* @media (min-width: 1024px) {
body { body {
display: flex; display: flex;
place-items: center; place-items: center;
} }
/* #app { #app {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
padding: 0 2rem; padding: 0 2rem;
} */ }
} } */
.scrollbar::-webkit-scrollbar { .scrollbar::-webkit-scrollbar {
background: transparent; background: transparent;
......
import { MessageType, ServiceFunc, type ParseDocOptions } from "@/types" import {
MessageType,
ServiceFunc,
type ParseDocOptions,
ContentScriptId,
} from "@/types"
import { waitMessage, tabUpdated, getLocal } from "@/utils/ext" import { waitMessage, tabUpdated, getLocal } from "@/utils/ext"
import { offscreen } from "./offscreen" import { offscreen } from "./offscreen"
import config from '@/assets/config.json' import config from "@/assets/config.json"
import { allFrameScript, mainContentScript } from "@/manifest"
type Config = typeof config
const manifest = chrome.runtime.getManifest()
const placeholder = manifest.content_scripts![0]
const contentScript = {
id: ContentScriptId.content,
matches: ["<all_urls>"],
js: placeholder.js,
runAt: placeholder.run_at as "document_start",
} satisfies chrome.scripting.RegisteredContentScript
chrome.scripting.registerContentScripts([contentScript, mainContentScript])
async function openPipBackground(url: string) { async function openPipBackground(url: string) {
const tab = await chrome.tabs.create({ const tab = await chrome.tabs.create({
...@@ -87,6 +107,7 @@ async function handleInvokeRequest( ...@@ -87,6 +107,7 @@ async function handleInvokeRequest(
break break
} }
} catch (err) { } catch (err) {
console.error("invoke error: ", err)
error = err error = err
} }
...@@ -155,18 +176,30 @@ function handleCommand(command: string) { ...@@ -155,18 +176,30 @@ function handleCommand(command: string) {
chrome.commands.onCommand.addListener(handleCommand) chrome.commands.onCommand.addListener(handleCommand)
async function updateConfig() { async function updateConfig() {
const url = 'https://config.ziziyi.com/anything-copilot' const url = "https://config.ziziyi.com/anything-copilot"
const now = Date.now() const now = Date.now()
let { timestamp, configVersion, chatDocSites } = await getLocal({ let {
timestamp,
configVersion,
chatDocSites,
webviewPatchs,
popularSites,
loadCandidates,
} = await getLocal({
timestamp: {} as Record<string, number>, timestamp: {} as Record<string, number>,
configVersion: config.data.configVersion, configVersion: config.data.configVersion,
chatDocSites: config.data.chatDocSites, chatDocSites: config.data.chatDocSites,
webviewPatchs: config.data.webviewPatchs,
popularSites: config.data.popularSites,
loadCandidates: config.data.loadCandidates,
}) })
if (timestamp.configUpdatedAt > 0 && now - timestamp.configUpdatedAt < 1000 * 60 * 60 * 24) { if (
timestamp.configUpdatedAt > 0 &&
now - timestamp.configUpdatedAt < 1000 * 60 * 60 * 24
) {
return return
} }
...@@ -177,14 +210,29 @@ async function updateConfig() { ...@@ -177,14 +210,29 @@ async function updateConfig() {
return return
} }
if (Array.isArray(data.chatDocSites) && data.chatDocSites.every((i: any) => i.host && i.selector)) { if (
Array.isArray(data.chatDocSites) &&
data.chatDocSites.every((i: any) => i.host && i.selector)
) {
chatDocSites = data.chatDocSites chatDocSites = data.chatDocSites
} }
if (data.webviewPatchs && Array.isArray(data.webviewPatchs)) {
webviewPatchs = data.webviewPatchs
}
if (data.popularSites && Array.isArray(data.popularSites)) {
popularSites = data.popularSites
}
if (data.loadCandidates && Array.isArray(data.loadCandidates)) {
loadCandidates = data.loadCandidates
}
timestamp.configUpdatedAt = now timestamp.configUpdatedAt = now
await chrome.storage.local.set({ await chrome.storage.local.set({
chatDocSites,
timestamp, timestamp,
chatDocSites,
webviewPatchs,
popularSites,
loadCandidates,
}) })
} }
......
...@@ -36,7 +36,7 @@ export async function setupOffscreenDocument(path: string) { ...@@ -36,7 +36,7 @@ export async function setupOffscreenDocument(path: string) {
} }
} }
export const offscreenHtmlPath = "/src/pages/offscreen.html" export const offscreenHtmlPath = "/offscreen.html"
class Offscreen extends Invoke { class Offscreen extends Invoke {
public readonly path: string public readonly path: string
...@@ -50,6 +50,8 @@ class Offscreen extends Invoke { ...@@ -50,6 +50,8 @@ class Offscreen extends Invoke {
const key = this.key const key = this.key
await this.setup() await this.setup()
console.log("offscreen send: ", key, req)
const response = await chrome.runtime.sendMessage({ const response = await chrome.runtime.sendMessage({
type: MessageType.toOffscreen, type: MessageType.toOffscreen,
key, key,
......
...@@ -3,7 +3,7 @@ import { ref, computed, watch, onMounted, onUnmounted } from "vue" ...@@ -3,7 +3,7 @@ import { ref, computed, watch, onMounted, onUnmounted } from "vue"
import IconMinimize from "@/components/icons/IconMinimize.vue" import IconMinimize from "@/components/icons/IconMinimize.vue"
import IconSplitRight from "@/components/icons/IconSplitscreenRight.vue" import IconSplitRight from "@/components/icons/IconSplitscreenRight.vue"
import IconClose from "@/components/icons/IconClose.vue" import IconClose from "@/components/icons/IconClose.vue"
import { MessageType } from "@/types" import { ContentEventType, MessageType } from "@/types"
import { pipWindow } from "@/store" import { pipWindow } from "@/store"
import { throttle } from "lodash-es" import { throttle } from "lodash-es"
import IconRefresh from "./icons/IconRefresh.vue" import IconRefresh from "./icons/IconRefresh.vue"
...@@ -140,7 +140,7 @@ const refresh = () => { ...@@ -140,7 +140,7 @@ const refresh = () => {
const win = pipWindow.window const win = pipWindow.window
if (win) { if (win) {
dispatchContentEvent({ dispatchContentEvent({
type: "load-doc", type: ContentEventType.pipLoad,
detail: { url: win.location.href }, detail: { url: win.location.href },
}) })
} }
......
...@@ -3,6 +3,7 @@ import { dispatchContentEvent } from "@/content/event" ...@@ -3,6 +3,7 @@ import { dispatchContentEvent } from "@/content/event"
import { reactive, ref } from "vue" import { reactive, ref } from "vue"
import IconClose from "./icons/IconClose.vue" import IconClose from "./icons/IconClose.vue"
import { useI18n } from "@/utils/i18n" import { useI18n } from "@/utils/i18n"
import { ContentEventType } from "@/types"
const { t } = useI18n() const { t } = useI18n()
...@@ -12,7 +13,7 @@ const host = ref(location.host) ...@@ -12,7 +13,7 @@ const host = ref(location.host)
function handleClick() { function handleClick() {
dispatchContentEvent({ dispatchContentEvent({
type: "pip", type: ContentEventType.pip,
detail: { detail: {
url: location.href, url: location.href,
mode: "write-html", mode: "write-html",
......
<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 { findFrameLoadUrl } from "@/utils/utils"
import { fetchDoc } from "@/content/pip"
const props = defineProps<{
url: string
}>()
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 onceCallback = new Map<string, (e: MessageEvent) => boolean>()
function handleFrameMessage(e: MessageEvent) {
console.log("frame message: ", e, e.source !== frame.value?.contentWindow)
if (e.source !== frame.value?.contentWindow) return
const type = e.data.type
if (!type) return
onceCallback.get(type)?.(e) && onceCallback.delete(type)
switch (type) {
case FrameMessageType.pageInfo:
if (!pageInfo.url) {
pageInfo.url = e.data.url
pageInfo.title = e.data.title
pageInfo.icon = e.data.icon
getLocal({
sidebarRecentItems: [] as {
url: string
icon: string
title: string
}[],
}).then(({ sidebarRecentItems }) => {
const index = sidebarRecentItems.findIndex(
(i) => i.url === pageInfo.url
)
if (index !== -1) {
sidebarRecentItems.splice(index, 1)
}
sidebarRecentItems.unshift(pageInfo)
sidebarRecentItems.splice(10)
chrome.storage.local.set({ sidebarRecentItems })
})
}
break
}
}
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
})
window.addEventListener("message", handleFrameMessage)
})
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 || "",
l: patch?.l || "",
}
})
watch(patch, async (patch) => {
const iframe = frame.value
if (!patch.url || !iframe) return
const tab = await chrome.tabs.getCurrent()
await updateFrameNetRules({
ua: patch.ua,
tabIds: [tab?.id || -1],
})
await chrome.scripting.updateContentScripts([
{
id: ContentScriptId.content,
allFrames: true,
},
{
id: ContentScriptId.main,
allFrames: true,
},
])
frameUrl.value = patch.url
pageInfo.url = ""
pageInfo.title = ""
pageInfo.icon = ""
const loadTimeout = 1000 * 1
try {
await new Promise<void>((resolve, reject) => {
onceCallback.set(FrameMessageType.frameReady, () => {
resolve()
return true
})
iframe.onload = () => {
setTimeout(() => reject(new Error("Frame load timeout")), loadTimeout)
}
})
} catch (e) {
console.warn(e)
const url = new URL(props.url)
let loadUrl = url.origin + patch.l
if (!loadUrl) {
const u = await findFrameLoadUrl(loadUrls)
u && (loadUrl = u)
}
try {
frameUrl.value = loadUrl
await new Promise<void>((resolve, reject) => {
onceCallback.set(FrameMessageType.frameReady, () => {
resolve()
return true
})
iframe.onload = () => {
setTimeout(() => reject(new Error("Frame load timeout")), loadTimeout)
}
})
iframe.contentWindow?.postMessage(
{
type: FrameMessageType.escapeLoad,
url: props.url,
},
"*"
)
} catch (e) {
console.warn(e)
frameUrl.value = ""
const res = await fetchDoc(props.url)
const html = await res.text()
// iframe.srcdoc = html
}
}
iframe.contentWindow?.postMessage(
{
type: FrameMessageType.contentRun,
},
"*"
)
})
</script>
<template>
<iframe class="w-full h-full" ref="frame" :src="frameUrl"></iframe>
</template>
<style scoped></style>
...@@ -125,10 +125,11 @@ onMounted(() => { ...@@ -125,10 +125,11 @@ onMounted(() => {
const { host, pathname } = doc.location const { host, pathname } = doc.location
const matchConfig = chatDocSites.find( const matchConfig = chatDocSites.find(
(s) => s.host == host && (new RegExp(s.path)).test(pathname) (s) => s.host == host && new RegExp(s.path).test(pathname)
) )
if (matchConfig) { if (matchConfig) {
siteConfig.maxInput = matchConfig.maxInputToken || matchConfig.maxInputLength siteConfig.maxInput =
matchConfig.maxInputToken || matchConfig.maxInputLength
siteConfig.maxInputType = matchConfig.maxInputToken ? "token" : "char" siteConfig.maxInputType = matchConfig.maxInputToken ? "token" : "char"
siteConfig.selector = matchConfig.selector siteConfig.selector = matchConfig.selector
} }
...@@ -147,15 +148,21 @@ onUnmounted(() => { ...@@ -147,15 +148,21 @@ onUnmounted(() => {
<template> <template>
<div ref="div" class="hidden"></div> <div ref="div" class="hidden"></div>
<div v-if="docsAddon.visible" :class="[ <div
v-if="docsAddon.visible"
:class="[
'fixed mt-10 top-0 p-6 border-2 rounded-lg bg-background z-[9999999]', 'fixed mt-10 top-0 p-6 border-2 rounded-lg bg-background z-[9999999]',
'shadow-lg left-1/2 -translate-x-1/2 w-max transition-all', 'shadow-lg left-1/2 -translate-x-1/2 w-max transition-all',
{ {
'border-primary/50': !docsAddon.active && docsAddon.visible, 'border-primary/50': !docsAddon.active && docsAddon.visible,
'border-primary scale-105': docsAddon.active, 'border-primary scale-105': docsAddon.active,
}, },
]" @dragenter="docsAddon.active = true" @dragleave="docsAddon.active = false" @dragover="(e) => e.preventDefault()" ]"
@drop="onDrop"> @dragenter="docsAddon.active = true"
@dragleave="docsAddon.active = false"
@dragover="(e) => e.preventDefault()"
@drop="onDrop"
>
<div class="pointer-events-none"> <div class="pointer-events-none">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<IconNoteStackAdd class="w-8 h-8" /> <IconNoteStackAdd class="w-8 h-8" />
...@@ -168,23 +175,39 @@ onUnmounted(() => { ...@@ -168,23 +175,39 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<div v-if="chatDocsPanel.visible" ref="chatDocsDiv" :class="[ <div
v-if="chatDocsPanel.visible"
ref="chatDocsDiv"
:class="[
'fixed flex flex-col w-96 max-w-full h-[600px] max-h-full border rounded-lg', 'fixed flex flex-col w-96 max-w-full h-[600px] max-h-full border rounded-lg',
'z-[9999] border-foreground/10 bg-background shadow-lg dark:border-2', 'z-[9999] border-foreground/10 bg-background shadow-lg dark:border-2',
{ {
'left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2': !position.valid, 'left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2': !position.valid,
}, },
]"> ]"
<div class="flex items-center px-4 pt-4 pb-1 select-none" @pointerdown="(e) => e.buttons == 1 && (e.target as Element)?.setPointerCapture(e.pointerId) >
" @pointermove="handlePointerMove" @pointerup="adjustPosition" @pointercancel="adjustPosition"> <div
class="flex items-center px-4 pt-4 pb-1 select-none"
@pointerdown="(e) => e.buttons == 1 && (e.target as Element)?.setPointerCapture(e.pointerId)
"
@pointermove="handlePointerMove"
@pointerup="adjustPosition"
@pointercancel="adjustPosition"
>
<img :src="logoUrl" class="w-6 h-6" /> <img :src="logoUrl" class="w-6 h-6" />
<span class="mx-2 text-xl font-bold">{{ t("chatDocsAddon") }}</span> <span class="mx-2 text-xl font-bold">{{ t("chatDocsAddon") }}</span>
<button aria-label="close" class="ml-auto p-1 top-0 right-0 rounded-full hover:bg-rose-400/10" <button
@click="chatDocsPanel.visible = false"> aria-label="close"
class="ml-auto p-1 top-0 right-0 rounded-full hover:bg-rose-400/10"
@click="chatDocsPanel.visible = false"
>
<IconClose class="w-5 h-5" /> <IconClose class="w-5 h-5" />
</button> </button>
</div> </div>
<ChatDocsPanel @close="chatDocsPanel.visible = false" :siteConfig="siteConfig" /> <ChatDocsPanel
@close="chatDocsPanel.visible = false"
:siteConfig="siteConfig"
/>
</div> </div>
</template> </template>
......
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 -960 960 960"
width="24"
fill="currentColor"
>
<path
d="M280-160v-640h400v640H280Zm-160-80v-480h80v480h-80Zm640 0v-480h80v480h-80Zm-400 0h240v-480H360v480Zm0 0v-480 480Z"
/>
</svg>
</template>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 -960 960 960"
width="24"
fill="currentColor"
>
<path
d="M480-320q17 0 28.5-11.5T520-360q0-17-11.5-28.5T480-400q-17 0-28.5 11.5T440-360q0 17 11.5 28.5T480-320Zm-40-160h80v-200h-80v200Zm40 400q-139-35-229.5-159.5T160-516v-244l320-120 320 120v244q0 152-90.5 276.5T480-80Zm0-84q104-33 172-132t68-220v-189l-240-90-240 90v189q0 121 68 220t172 132Zm0-316Z"
/>
</svg>
</template>
...@@ -39,45 +39,35 @@ async function closePip() { ...@@ -39,45 +39,35 @@ async function closePip() {
</script> </script>
<template> <template>
<div v-if="pipWindow.tab && pipWindow.tab.id"> <div class="justify-between border-2 border-solid border-background-mute">
<div class="text-sm flex items-center truncate mt-6"> <div
<span class="size-7 rounded mr-auto"
class="w-4 h-4 inline-block mr-2 rounded"
:style="{ :style="{
background: background: `#8881 center / contain url('${pipWindow.tab?.favIconUrl}')`,
'#8882 center / contain url(' + pipWindow.tab?.favIconUrl + ')',
}" }"
></span> ></div>
<span>{{ pipWindow.tab?.title }}</span>
</div>
<div class="flex gap-2">
<button <button
class="primary-btn flex items-center mt-2 rounded-lg p-2 px-3" v-if="pipWindow.windowsWindow?.state === 'normal'"
class="bg-background-soft hover:bg-background-mute rounded-full size-8 p-1 flex items-center justify-center"
@click="handleUpdatePip('minimized')" @click="handleUpdatePip('minimized')"
> >
<IconHide /> <IconHide class="size-5" />
</button> </button>
<button <button
class="primary-btn flex items-center mt-2 rounded-lg p-2 px-3" v-if="pipWindow.windowsWindow?.state === 'minimized'"
class="bg-background-soft hover:bg-background-mute rounded-full size-8 p-1 flex items-center justify-center"
@click="handleUpdatePip('normal')" @click="handleUpdatePip('normal')"
> >
<IconArrowCircleRight /> <IconArrowCircleRight class="size-5" />
</button> </button>
<button <button
class="primary-btn flex items-center mt-2 rounded-lg p-2 px-3" class="bg-background-soft hover:bg-background-mute rounded-full size-8 p-1 flex items-center justify-center"
@click="closePip" @click="closePip"
> >
<IconClose /> <IconClose class="size-5" />
</button> </button>
</div> </div>
</div>
</template> </template>
<style scoped> <style scoped></style>
.primary-btn {
background: var(--color-background-soft);
}
.primary-btn:hover {
background: var(--color-background-mute);
}
</style>
...@@ -16,5 +16,3 @@ const { t } = useI18n() ...@@ -16,5 +16,3 @@ const { t } = useI18n()
<ChatDocsAddon /> <ChatDocsAddon />
</template> </template>
<style scoped></style>
import type { ContentEventType } from "@/types"
type EventOptions = type EventOptions =
| { | {
type: "pip" type: ContentEventType.pip
detail: { detail: {
url: string url: string
mode: string mode: string
} }
} }
| { | {
type: "load-doc" type: ContentEventType.pipLoad
detail: { detail: {
url: string url: string
} }
} }
| { | {
type: "loaded" type: ContentEventType.pipLoaded
detail: {} detail: {}
} }
| {
type EventType = EventOptions["type"] type: ContentEventType.escapeLoad
detail: {
function getRealType(type: string) { url: string
return "anything-copilot_" + type }
} }
export function dispatchContentEvent({ type, detail }: EventOptions) { export function dispatchContentEvent({ type, detail }: EventOptions) {
const event = new CustomEvent(getRealType(type), { detail }) const event = new CustomEvent(type, { detail })
document.dispatchEvent(event) document.dispatchEvent(event)
} }
export function addContentEventListener( export function addContentEventListener(
type: EventType, type: ContentEventType,
handler: (e: Event) => void handler: (e: Event) => void
) { ) {
document.addEventListener(getRealType(type), handler) document.addEventListener(type, handler)
} }
export function removeContentEventListener( export function removeContentEventListener(
type: EventType, type: ContentEventType,
handler: (e: Event) => void handler: (e: Event) => void
) { ) {
document.removeEventListener(getRealType(type), handler) document.removeEventListener(type, handler)
} }
import { MessageType } from "@/types"
chrome.runtime.sendMessage({
type: MessageType.frameReady,
})
import { mount, waitMountApp } from "./ui" import { mount, waitMountApp } from "./ui"
import { import { chatDocsPanel, pipLauncher, pipLoading, pipWindow } from "@/store"
chatDocsPanel, import { ContentEventType, FrameMessageType, MessageType } from "@/types"
pipLauncher,
pipLoading,
pipWindow,
} from "@/store"
import { MessageType } from "@/types"
import Copilot from "./Copilot.vue" import Copilot from "./Copilot.vue"
import { waitMessage } from "@/utils/ext" import { waitMessage } from "@/utils/ext"
import { import {
...@@ -14,6 +9,7 @@ import { ...@@ -14,6 +9,7 @@ import {
removeContentEventListener, removeContentEventListener,
} from "@/content/event" } from "@/content/event"
import { contentService } from "@/utils/service" import { contentService } from "@/utils/service"
import { getPageIcon } from "@/utils/dom"
// import { PipEventName } from "@/types/pip" // import { PipEventName } from "@/types/pip"
function handleMessage( function handleMessage(
...@@ -25,7 +21,7 @@ function handleMessage( ...@@ -25,7 +21,7 @@ function handleMessage(
switch (message?.type) { switch (message?.type) {
case MessageType.pip: case MessageType.pip:
dispatchContentEvent({ dispatchContentEvent({
type: "pip", type: ContentEventType.pip,
detail: message.options, detail: message.options,
}) })
break break
...@@ -73,9 +69,9 @@ async function handlePipEvent(event: any) { ...@@ -73,9 +69,9 @@ async function handlePipEvent(event: any) {
const handlePipLoaded = (e: Event) => { const handlePipLoaded = (e: Event) => {
console.log("load", e) console.log("load", e)
r() r()
removeContentEventListener("loaded", handlePipLoaded) removeContentEventListener(ContentEventType.pipLoaded, handlePipLoaded)
} }
addContentEventListener("loaded", handlePipLoaded) addContentEventListener(ContentEventType.pipLoaded, handlePipLoaded)
}) })
// may be 0 if not wait document is loaded // may be 0 if not wait document is loaded
...@@ -109,11 +105,52 @@ async function handlePopLoadDocEvent(e: CustomEvent | Event) { ...@@ -109,11 +105,52 @@ async function handlePopLoadDocEvent(e: CustomEvent | Event) {
pipLoading.isLoading = true pipLoading.isLoading = true
} }
chrome.runtime?.onMessage.addListener(handleMessage) function run() {
addContentEventListener("pip", handlePipEvent) chrome.runtime?.onMessage.addListener(handleMessage)
addContentEventListener("loaded", handlePipLoadedEvent) addContentEventListener(ContentEventType.pip, handlePipEvent)
addContentEventListener("load-doc", handlePopLoadDocEvent) addContentEventListener(ContentEventType.pipLoaded, handlePipLoadedEvent)
waitMountApp() addContentEventListener(ContentEventType.pipLoad, handlePopLoadDocEvent)
waitMountApp()
}
async function postPageInfo() {
await new Promise((r) => setTimeout(r, 1000 * 3))
window.top?.postMessage(
{
type: FrameMessageType.pageInfo,
url: location.href,
title: document.title || location.host,
icon: getPageIcon(),
},
chrome.runtime.getURL("")
)
}
if (window.self == window.top) {
run()
} else {
window.addEventListener("message", (e) => {
if (!e.data || typeof e.data !== "object") return
const type = e.data.type
switch (type) {
case FrameMessageType.contentRun:
run()
postPageInfo()
return
case FrameMessageType.escapeLoad:
return dispatchContentEvent({
type: ContentEventType.escapeLoad,
detail: { url: e.data.url },
})
}
})
window.top?.postMessage(
{
type: FrameMessageType.frameReady,
},
chrome.runtime.getURL("")
)
}
// dev // dev
if (location.host == chrome.runtime.id && location.hash == "#copilot") { if (location.host == chrome.runtime.id && location.hash == "#copilot") {
......
import { ContentEventType } from "@/types"
import { addContentEventListener } from "./event" import { addContentEventListener } from "./event"
import { copilotNavigateTo, pip } from "./pip" import { copilotNavigateTo, pip, fetchDoc, writeHtml } from "./pip"
function handlePipEvent(event: CustomEvent | Event) { function handlePipEvent(event: CustomEvent | Event) {
if ("detail" in event) { if ("detail" in event) {
...@@ -7,11 +8,22 @@ function handlePipEvent(event: CustomEvent | Event) { ...@@ -7,11 +8,22 @@ function handlePipEvent(event: CustomEvent | Event) {
} }
} }
function handleLoadDocEvent(event: CustomEvent | Event) { function handlePipLoadDocEvent(event: CustomEvent | Event) {
if ("detail" in event) { if ("detail" in event) {
copilotNavigateTo(event.detail.url) copilotNavigateTo(event.detail.url)
} }
} }
addContentEventListener('pip', handlePipEvent) async function handleEscapeLoadEvent(event: CustomEvent | Event) {
addContentEventListener('load-doc', handleLoadDocEvent) if ("detail" in event) {
const url = event.detail.url
const res = await fetchDoc(url)
const html = await res.text()
window.history.replaceState(window.history.state, "", url)
writeHtml(window, html)
}
}
addContentEventListener(ContentEventType.pip, handlePipEvent)
addContentEventListener(ContentEventType.pipLoad, handlePipLoadDocEvent)
addContentEventListener(ContentEventType.escapeLoad, handleEscapeLoadEvent)
...@@ -8,8 +8,9 @@ import { ...@@ -8,8 +8,9 @@ import {
removePrerenderRules, removePrerenderRules,
} from "@/utils/dom" } from "@/utils/dom"
import { dispatchContentEvent } from "./event" import { dispatchContentEvent } from "./event"
import { ContentEventType } from "@/types"
function fetchDoc(input: URL | RequestInfo, init?: RequestInit) { export function fetchDoc(input: URL | RequestInfo, init?: RequestInit) {
const headers = { const headers = {
Accept: Accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
...@@ -85,8 +86,8 @@ export async function copilotNavigateTo(url: string) { ...@@ -85,8 +86,8 @@ export async function copilotNavigateTo(url: string) {
const res = await fetchDoc(url) const res = await fetchDoc(url)
const html = await res.text() const html = await res.text()
// resetWindow(pipWindow); // resetWindow(pipWindow);
writeHtml(pipWindow, html)
pipWindow.history.replaceState(pipWindow.history.state, "", url) pipWindow.history.replaceState(pipWindow.history.state, "", url)
writeHtml(pipWindow, html)
} }
type ReopenOptions = { type ReopenOptions = {
...@@ -115,7 +116,7 @@ export async function copilotReopen({ url, width, height }: ReopenOptions) { ...@@ -115,7 +116,7 @@ export async function copilotReopen({ url, width, height }: ReopenOptions) {
navGuard(pipWindow) navGuard(pipWindow)
} }
function writeHtml(pipWindow: Window, html: string) { export function writeHtml(pipWindow: Window, html: string) {
const nonce = getDomNonce(document) const nonce = getDomNonce(document)
let escaped = replaceHtmlNonce(html, nonce) let escaped = replaceHtmlNonce(html, nonce)
escaped = getTrustedHTML(escaped) escaped = getTrustedHTML(escaped)
...@@ -123,7 +124,7 @@ function writeHtml(pipWindow: Window, html: string) { ...@@ -123,7 +124,7 @@ function writeHtml(pipWindow: Window, html: string) {
pipWindow.document.open() pipWindow.document.open()
pipWindow.document.write(escaped) pipWindow.document.write(escaped)
pipWindow.document.close() pipWindow.document.close()
dispatchContentEvent({ type: "loaded", detail: {} }) dispatchContentEvent({ type: ContentEventType.pipLoaded, detail: {} })
const base = document.createElement("base") const base = document.createElement("base")
base.target = "_blank" base.target = "_blank"
......
...@@ -2,7 +2,8 @@ import { createApp, type Component } from "vue" ...@@ -2,7 +2,8 @@ import { createApp, type Component } from "vue"
import App from "./App.vue" import App from "./App.vue"
import { MessageType } from "@/types" import { MessageType } from "@/types"
import { i18n } from "@/utils/i18n" import { i18n } from "@/utils/i18n"
import "@/assets/main.css" // import "@/assets/main.css"
import styles from "@/assets/main.css?inline"
const isSelf = chrome.runtime?.id === location.host const isSelf = chrome.runtime?.id === location.host
...@@ -11,11 +12,17 @@ export function mount(App: Component, doc = document) { ...@@ -11,11 +12,17 @@ export function mount(App: Component, doc = document) {
const root = isSelf ? outter : outter.attachShadow({ mode: "open" }) const root = isSelf ? outter : outter.attachShadow({ mode: "open" })
const appContainer = doc.createElement("div") const appContainer = doc.createElement("div")
appContainer.id = "app" appContainer.id = "app"
// appContainer.setAttribute("part", "app")
const link = doc.createElement("link") const link = doc.createElement("link")
link.rel = "stylesheet" link.rel = "stylesheet"
link.href = chrome.runtime?.getURL("/assets/index.css") link.href = chrome.runtime?.getURL("/assets/index.css")
const style = doc.createElement("style")
style.innerHTML = styles
root.append(link) root.append(link)
root.append(style)
root.append(appContainer) root.append(appContainer)
doc.documentElement.append(outter) doc.documentElement.append(outter)
...@@ -30,7 +37,7 @@ export function mountApp(doc = document) { ...@@ -30,7 +37,7 @@ export function mountApp(doc = document) {
} }
export function waitMountApp() { export function waitMountApp() {
if (document.readyState == "interactive") { if (["interactive", "complete"].includes(document.readyState)) {
mountApp() mountApp()
} else { } else {
const hanldeStateChange = () => { const hanldeStateChange = () => {
......
{ {
"openInPip": "በCopilot የምጥ ገጽ ክፍል ክፈት", "openInPip": "ብቅ ባለበት ክፍት ነው",
"other": "ሌሎች", "openInSidebar": "በጎን አሞሌ ውስጥ ይክፈቱ",
"popular": "ታዋቂ",
"sidebar": "የጎን አሞሌ",
"clickHere": "እዚህ ጠቅ ያድርጉ", "clickHere": "እዚህ ጠቅ ያድርጉ",
"minimize": "ምንጭ", "minimize": "ምንጭ",
"moveAside": "ቀስት ለማግኘት", "moveAside": "ቀስት ለማግኘት",
......
{ {
"openInPip": "افتح في نافذة كوبيلوت", "openInPip": "مفتوح في المنبثقة",
"other": "آخر", "openInSidebar": "مفتوح في الشريط الجانبي",
"popular": "شائع",
"sidebar": "الشريط الجانبي",
"clickHere": "انقر هنا", "clickHere": "انقر هنا",
"minimize": "تصغير", "minimize": "تصغير",
"moveAside": "تحريك جانباً", "moveAside": "تحريك جانباً",
......
{ {
"openInPip": "Отвори в Copilot прозорец", "openInPip": "Отворен в изскачащ прозорец",
"other": "Друго", "openInSidebar": "Отворен в страничната лента",
"popular": "Популярен",
"sidebar": "Странична лента",
"clickHere": "Цъкни тук", "clickHere": "Цъкни тук",
"minimize": "Минимизиране", "minimize": "Минимизиране",
"moveAside": "Премести настрана", "moveAside": "Премести настрана",
......
{ {
"openInPip": "Copilot উইন্ডোতে খুলুন", "openInPip": "পপআপে খুলুন",
"other": "অন্যান্য", "openInSidebar": "সাইডবারে খোলা",
"popular": "জনপ্রিয়",
"sidebar": "সাইডবার",
"clickHere": "এখানে ক্লিক করুন", "clickHere": "এখানে ক্লিক করুন",
"minimize": "সর্বনিম্ন", "minimize": "সর্বনিম্ন",
"moveAside": "পাশে সরান", "moveAside": "পাশে সরান",
......
{ {
"openInPip": "Obri en finestra del Copilot", "openInPip": "Obert a la finestra emergent",
"other": "Altres", "openInSidebar": "Obert a la barra lateral",
"popular": "Popular",
"sidebar": "Barra lateral",
"clickHere": "Clica aquí", "clickHere": "Clica aquí",
"minimize": "Minimitza", "minimize": "Minimitza",
"moveAside": "Aparta", "moveAside": "Aparta",
......
{ {
"openInPip": "Otevřít v okně Copilot", "openInPip": "Otevřeno v vyskakovacím okně",
"other": "Další", "openInSidebar": "Otevřeno v postranním panelu",
"popular": "Oblíbený",
"sidebar": "Postranní panel",
"clickHere": "Klikněte zde", "clickHere": "Klikněte zde",
"minimize": "Minimalizovat", "minimize": "Minimalizovat",
"moveAside": "Přesunout stranou", "moveAside": "Přesunout stranou",
......
{ {
"openInPip": "Åbn i Copilot-vindue", "openInPip": "Åben i popup",
"other": "Andet", "openInSidebar": "Åben i sidebjælken",
"popular": "Populær",
"sidebar": "Sidebjælke",
"clickHere": "Klik her", "clickHere": "Klik her",
"minimize": "Minimer", "minimize": "Minimer",
"moveAside": "Flyt til side", "moveAside": "Flyt til side",
......
{ {
"openInPip": "In Copilot-Fenster öffnen", "openInPip": "Offen im Popup",
"other": "Andere", "openInSidebar": "In der Seitenleiste geöffnet",
"popular": "Beliebt",
"sidebar": "Seitenleiste",
"clickHere": "Hier klicken", "clickHere": "Hier klicken",
"minimize": "Minimieren", "minimize": "Minimieren",
"moveAside": "Beiseite bewegen", "moveAside": "Beiseite bewegen",
......
{ {
"openInPip": "Ανοίξτε στο παράθυρο του Copilot", "openInPip": "Ανοίξτε στο αναδυόμενο παράθυρο",
"other": "Άλλο", "openInSidebar": "Ανοίξτε στην πλαϊνή μπάρα",
"popular": "Δημοφιλής",
"sidebar": "Πλευρική γραμμή",
"clickHere": "Κάντε κλικ εδώ", "clickHere": "Κάντε κλικ εδώ",
"minimize": "Ελαχιστοποίηση", "minimize": "Ελαχιστοποίηση",
"moveAside": "Μετακίνηση στην πλευρά", "moveAside": "Μετακίνηση στην πλευρά",
......
{ {
"openInPip": "Open in Copilot window", "openInPip": "Open in Popup",
"other": "Other", "openInSidebar": "Open in Sidebar",
"popular": "Popular",
"sidebar": "Sidebar",
"clickHere": "Click Here", "clickHere": "Click Here",
"minimize": "Minimize", "minimize": "Minimize",
"moveAside": "Move aside", "moveAside": "Move aside",
......
{ {
"openInPip": "Abrir en ventana de Copilot", "openInPip": "Abrir en ventana emergente",
"other": "Otro", "openInSidebar": "Abrir en la barra lateral",
"popular": "Popular",
"sidebar": "Barra lateral",
"clickHere": "Haz clic aquí", "clickHere": "Haz clic aquí",
"minimize": "Minimizar", "minimize": "Minimizar",
"moveAside": "Mover a un lado", "moveAside": "Mover a un lado",
......
{ {
"openInPip": "Abrir en ventana de Copilot", "openInPip": "Abrir en ventana emergente",
"other": "Otro", "openInSidebar": "Abrir en la barra lateral",
"popular": "Popular",
"sidebar": "Barra lateral",
"clickHere": "Haz clic aquí", "clickHere": "Haz clic aquí",
"minimize": "Minimizar", "minimize": "Minimizar",
"moveAside": "Mover a un lado", "moveAside": "Mover a un lado",
......
{ {
"openInPip": "Avage Copiloti aken", "openInPip": "Avatud hüpikus",
"other": "Muu", "openInSidebar": "Avatud külgribal",
"popular": "Populaarne",
"sidebar": "Külgriba",
"clickHere": "Klõpsake siin", "clickHere": "Klõpsake siin",
"minimize": "Vähenda", "minimize": "Vähenda",
"moveAside": "Liiguta kõrvale", "moveAside": "Liiguta kõrvale",
......
{ {
"openInPip": "باز کردن در پنجره Copilot", "openInPip": "باز در پنجره",
"other": "سایر", "openInSidebar": "در نوار کناری باز می شود",
"popular": "محبوب",
"sidebar": "نوار کناری",
"clickHere": "اینجا کلیک کنید", "clickHere": "اینجا کلیک کنید",
"minimize": "کمینه کردن", "minimize": "کمینه کردن",
"moveAside": "جابجا شدن", "moveAside": "جابجا شدن",
......
{ {
"openInPip": "Avaa Copilot-ikkunassa", "openInPip": "Avoin ponnahdusikkuna",
"other": "Muu", "openInSidebar": "Avoinna sivupalkissa",
"popular": "Suosittu",
"sidebar": "Sivupalkki",
"clickHere": "Klikkaa tästä", "clickHere": "Klikkaa tästä",
"minimize": "Pienennä", "minimize": "Pienennä",
"moveAside": "Siirrä sivuun", "moveAside": "Siirrä sivuun",
......
{ {
"openInPip": "Buksan sa bintana ng Copilot", "openInPip": "Buksan sa Popup",
"other": "Iba pa", "openInSidebar": "Buksan sa sidebar",
"popular": "Tanyag",
"sidebar": "Sidebar",
"clickHere": "I-click Dito", "clickHere": "I-click Dito",
"minimize": "Ibawas", "minimize": "Ibawas",
"moveAside": "Ilipat sa tabi", "moveAside": "Ilipat sa tabi",
......
{ {
"openInPip": "Ouvrir dans la fenêtre Copilot", "openInPip": "Ouvert en popup",
"other": "Autre", "openInSidebar": "Ouvert dans la barre latérale",
"popular": "Populaire",
"sidebar": "Barre latérale",
"clickHere": "Cliquez ici", "clickHere": "Cliquez ici",
"minimize": "Minimiser", "minimize": "Minimiser",
"moveAside": "Déplacer de côté", "moveAside": "Déplacer de côté",
......
{ {
"openInPip": "કોપિલોટ વિંડોમાં ખોલો", "openInPip": "પ pop પઅપ ખોલો",
"other": "અન્ય", "openInSidebar": "સાઇડબારમાં ખોલો",
"popular": "પ્રખ્યાત",
"sidebar": "લારીક",
"clickHere": "અહીં ક્લિક કરો", "clickHere": "અહીં ક્લિક કરો",
"minimize": "ઘટાડો", "minimize": "ઘટાડો",
"moveAside": "અપર ચાલો", "moveAside": "અપર ચાલો",
......
{ {
"openInPip": "פתח בחלון Copilot", "openInPip": "פתוח בקופץ",
"other": "אחר", "openInSidebar": "פתוח בסרגל הצד",
"popular": "פופולרי",
"sidebar": "סרגל צד",
"clickHere": "לחץ כאן", "clickHere": "לחץ כאן",
"minimize": "מזער", "minimize": "מזער",
"moveAside": "הזז בצד", "moveAside": "הזז בצד",
......
{ {
"openInPip": "कोपाइलट विंडो में खोलें", "openInPip": "पॉपअप में खुला",
"other": "अन्य", "openInSidebar": "साइडबार में खुला",
"popular": "लोकप्रिय",
"sidebar": "साइड बार",
"clickHere": "यहां क्लिक करें", "clickHere": "यहां क्लिक करें",
"minimize": "कम करें", "minimize": "कम करें",
"moveAside": "दूर हटें", "moveAside": "दूर हटें",
......
{ {
"openInPip": "Otvori u CoPilot prozoru", "openInPip": "Otvoreno u skočnom prozoru",
"other": "Ostalo", "openInSidebar": "Otvoreno na bočnoj traci",
"popular": "Popularan",
"sidebar": "Bočna traka",
"clickHere": "Kliknite ovdje", "clickHere": "Kliknite ovdje",
"minimize": "Smanji", "minimize": "Smanji",
"moveAside": "Pomakni na stranu", "moveAside": "Pomakni na stranu",
......
{ {
"openInPip": "Megnyitás a Copilot ablakban", "openInPip": "Nyissa meg a felbukkanó felbukkanást",
"other": "Egyéb", "openInSidebar": "Nyitva az oldalsávban",
"popular": "Népszerű",
"sidebar": "Oldalsáv",
"clickHere": "Kattintson ide", "clickHere": "Kattintson ide",
"minimize": "Kis méret", "minimize": "Kis méret",
"moveAside": "Elmozdít", "moveAside": "Elmozdít",
......
{ {
"openInPip": "Buka dalam jendela Copilot", "openInPip": "Buka di popup",
"other": "Lainnya", "openInSidebar": "Buka di bilah sisi",
"popular": "Populer",
"sidebar": "Bilah sisi",
"clickHere": "Klik Disini", "clickHere": "Klik Disini",
"minimize": "Miminimalkan", "minimize": "Miminimalkan",
"moveAside": "Pindah ke samping", "moveAside": "Pindah ke samping",
......
{ {
"openInPip": "Apri nella finestra di Copilot", "openInPip": "Aperto nel popup",
"other": "Altro", "openInSidebar": "Aperto nella barra laterale",
"popular": "Popolare",
"sidebar": "Barra laterale",
"clickHere": "Clicca qui", "clickHere": "Clicca qui",
"minimize": "Minimizza", "minimize": "Minimizza",
"moveAside": "Sposta da parte", "moveAside": "Sposta da parte",
......
{ {
"openInPip": "コパイロットウィンドウで開く", "openInPip": "ポップアップで開いています",
"other": "その他", "openInSidebar": "サイドバーで開きます",
"popular": "人気のある",
"sidebar": "サイドバー",
"clickHere": "ここをクリック", "clickHere": "ここをクリック",
"minimize": "最小化", "minimize": "最小化",
"moveAside": "一旦寄せる", "moveAside": "一旦寄せる",
......
{ {
"openInPip": "ಕೊಪಿಲೋಟ್ ವಿಂಡೋದಲ್ಲಿ ತೆರೆಯಿರಿ", "openInPip": "ಪಾಪ್ಅಪ್ನಲ್ಲಿ ತೆರೆಯಿರಿ",
"other": "ಇತರೆ", "openInSidebar": "ಸೈಡ್‌ಬಾರ್‌ನಲ್ಲಿ ತೆರೆಯಿರಿ",
"popular": "ಜನಪ್ರಿಯ",
"sidebar": "ಪಕ್ಕದ ಬಾರ್ನ",
"clickHere": "ಇಲ್ಲಿ ಕ್ಲಿಕ್ ಮಾಡಿ", "clickHere": "ಇಲ್ಲಿ ಕ್ಲಿಕ್ ಮಾಡಿ",
"minimize": "ಕುಗ್ಗಿಸಿ", "minimize": "ಕುಗ್ಗಿಸಿ",
"moveAside": "ಬೆಳಕು ನೀಡಿ", "moveAside": "ಬೆಳಕು ನೀಡಿ",
......
{ {
"openInPip": "Copilot 창에서 열기", "openInPip": "팝업에서 열립니다",
"other": "기타", "openInSidebar": "사이드 바에서 열립니다",
"popular": "인기 있는",
"sidebar": "사이드 바",
"clickHere": "여기를 클릭하세요", "clickHere": "여기를 클릭하세요",
"minimize": "최소화", "minimize": "최소화",
"moveAside": "옆으로 이동", "moveAside": "옆으로 이동",
......
{ {
"openInPip": "Atidaryti „Copilot“ langą", "openInPip": "Atidaryti iššokantįjį",
"other": "Kitas", "openInSidebar": "Atidarykite šoninėje juostoje",
"popular": "Populiarus",
"sidebar": "Šoninė juosta",
"clickHere": "Spustelėkite čia", "clickHere": "Spustelėkite čia",
"minimize": "Mažinti", "minimize": "Mažinti",
"moveAside": "Nustumti šalin", "moveAside": "Nustumti šalin",
......
{ {
"openInPip": "Atvērt Copilot logā", "openInPip": "Atvērts uznirstošajā logā",
"other": "Cits", "openInSidebar": "Atveriet sānjoslā",
"popular": "Populārs",
"sidebar": "Sānjosla",
"clickHere": "Noklikšķiniet šeit", "clickHere": "Noklikšķiniet šeit",
"minimize": "Minimizēt", "minimize": "Minimizēt",
"moveAside": "Pagriezt malā", "moveAside": "Pagriezt malā",
......
{ {
"openInPip": "കോപിലോറ്റ് ജിപ്പി ജോളം തുറക്കുക", "openInPip": "പോപ്പ്അപ്പിൽ തുറക്കുക",
"other": "മറ്റ്", "openInSidebar": "സൈഡ്ബാറിൽ തുറക്കുക",
"popular": "ജനപീതിയായ",
"sidebar": "സൈഡ്ബാർ",
"clickHere": "ഇവിടെ ക്ലിക്ക് ചെയ്യുക", "clickHere": "ഇവിടെ ക്ലിക്ക് ചെയ്യുക",
"minimize": "ഇടത്തരിക്കുക", "minimize": "ഇടത്തരിക്കുക",
"moveAside": "വലത്തരിക്കുക", "moveAside": "വലത്തരിക്കുക",
......
{ {
"openInPip": "कोपायलटच्या विंडोमध्ये उघडा", "openInPip": "पॉपअप मध्ये उघडा",
"other": "इतर", "openInSidebar": "साइडबार मध्ये उघडा",
"popular": "लोकप्रिय",
"sidebar": "साइडबार",
"clickHere": "येथे क्लिक करा", "clickHere": "येथे क्लिक करा",
"minimize": "कमी करा", "minimize": "कमी करा",
"moveAside": "पाठवा", "moveAside": "पाठवा",
......
{ {
"openInPip": "Buka dalam tetingkap Copilot", "openInPip": "Buka dalam popup",
"other": "Lain-lain", "openInSidebar": "Buka di bar sisi",
"popular": "Popular",
"sidebar": "Sidebar",
"clickHere": "Klik di sini", "clickHere": "Klik di sini",
"minimize": "Kurangkan", "minimize": "Kurangkan",
"moveAside": "Bersisih", "moveAside": "Bersisih",
......
{ {
"openInPip": "Open in Copilot-venster", "openInPip": "Open in pop -up",
"other": "Andere", "openInSidebar": "Open in zijbalk",
"popular": "Populair",
"sidebar": "Zijbalk",
"clickHere": "Klik hier", "clickHere": "Klik hier",
"minimize": "Minimaliseren", "minimize": "Minimaliseren",
"moveAside": "Opzij zetten", "moveAside": "Opzij zetten",
......
{ {
"openInPip": "Åpne i Copilot-vindu", "openInPip": "Åpent i popup",
"other": "Annet", "openInSidebar": "Åpent i sidefeltet",
"popular": "Populær",
"sidebar": "Sidefelt",
"clickHere": "Klikk her", "clickHere": "Klikk her",
"minimize": "Minimer", "minimize": "Minimer",
"moveAside": "Flytt til side", "moveAside": "Flytt til side",
......
{ {
"openInPip": "Otwórz w oknie Copilot", "openInPip": "Otwarte w wyskakującym okienku",
"other": "Inne", "openInSidebar": "Otwarte na pasku bocznym",
"popular": "Popularny",
"sidebar": "Pasek boczny",
"clickHere": "Kliknij tutaj", "clickHere": "Kliknij tutaj",
"minimize": "Zminimalizuj", "minimize": "Zminimalizuj",
"moveAside": "Przesuń na bok", "moveAside": "Przesuń na bok",
......
{ {
"openInPip": "Abrir na janela do Copilot", "openInPip": "Aberto em pop -up",
"other": "Outro", "openInSidebar": "Aberto na barra lateral",
"popular": "Popular",
"sidebar": "Barra Lateral",
"clickHere": "Clique aqui", "clickHere": "Clique aqui",
"minimize": "Minimizar", "minimize": "Minimizar",
"moveAside": "Mover para o lado", "moveAside": "Mover para o lado",
......
{ {
"openInPip": "Abrir em janela do Copilot", "openInPip": "Aberto em pop -up",
"other": "Outro", "openInSidebar": "Aberto na barra lateral",
"popular": "Popular",
"sidebar": "Barra Lateral",
"clickHere": "Clique aqui", "clickHere": "Clique aqui",
"minimize": "Minimizar", "minimize": "Minimizar",
"moveAside": "Mover para o lado", "moveAside": "Mover para o lado",
......
{ {
"openInPip": "Deschide în fereastra Copilot", "openInPip": "Deschis în pop -up",
"other": "Altele", "openInSidebar": "Deschis în bara laterală",
"popular": "Popular",
"sidebar": "Bara laterală",
"clickHere": "Click aici", "clickHere": "Click aici",
"minimize": "Minimizează", "minimize": "Minimizează",
"moveAside": "Mută în lateral", "moveAside": "Mută în lateral",
......
{ {
"openInPip": "Открыть в окне Copilot", "openInPip": "Открыт во всплывающем окне",
"other": "Другое", "openInSidebar": "Открыт на боковой панели",
"popular": "Популярный",
"sidebar": "Боковая панель",
"clickHere": "Нажмите здесь", "clickHere": "Нажмите здесь",
"minimize": "Минимизировать", "minimize": "Минимизировать",
"moveAside": "Убрать в сторону", "moveAside": "Убрать в сторону",
......
{ {
"openInPip": "Otvoriť v okne Copilot", "openInPip": "Otvorené v kontextovom konte",
"other": "Iné", "openInSidebar": "Otvorené na bočnom paneli",
"popular": "Populárny",
"sidebar": "Bočný panel",
"clickHere": "Kliknite sem", "clickHere": "Kliknite sem",
"minimize": "Minimalizovať", "minimize": "Minimalizovať",
"moveAside": "Presunúť bokom", "moveAside": "Presunúť bokom",
......
{ {
"openInPip": "Odpri v oknu Copilot", "openInPip": "Odprt v pojavnem oknu",
"other": "Drugo", "openInSidebar": "Odprto v stranski vrstici",
"popular": "Priljubljen",
"sidebar": "Stranska vrstica",
"clickHere": "Klikni tukaj", "clickHere": "Klikni tukaj",
"minimize": "Minimaliziraj", "minimize": "Minimaliziraj",
"moveAside": "Premakni na stran", "moveAside": "Premakni na stran",
......
{ {
"openInPip": "Отвори у прозору Копилота", "openInPip": "Отворен у скочном прозору",
"other": "Остало", "openInSidebar": "Отвори у бочној траци",
"popular": "Популаран",
"sidebar": "Бочна трака",
"clickHere": "Кликни овде", "clickHere": "Кликни овде",
"minimize": "Minimiziraj", "minimize": "Minimiziraj",
"moveAside": "Pomeri na stranu", "moveAside": "Pomeri na stranu",
......
{ {
"openInPip": "Öppna i Copilot-fönster", "openInPip": "Öppen i popup",
"other": "Annan", "openInSidebar": "Öppet i sidofältet",
"popular": "Populär",
"sidebar": "Sidofält",
"clickHere": "Klicka här", "clickHere": "Klicka här",
"minimize": "Minimera", "minimize": "Minimera",
"moveAside": "Flytta åt sidan", "moveAside": "Flytta åt sidan",
......
{ {
"openInPip": "Fungua kwenye dirisha la Copilot", "openInPip": "Fungua katika kidukizo",
"other": "Nyingine", "openInSidebar": "Fungua katika pembeni",
"popular": "Maarufu",
"sidebar": "Pembeni",
"clickHere": "Bonyeza Hapa", "clickHere": "Bonyeza Hapa",
"minimize": "Kupunguza", "minimize": "Kupunguza",
"moveAside": "Hama kando", "moveAside": "Hama kando",
......
{ {
"openInPip": "கிளிக் மேல் இங்கே திற", "openInPip": "பாப்அப்பில் திறந்திருக்கும்",
"other": "பிற", "openInSidebar": "பக்கப்பட்டியில் திறக்கவும்",
"popular": "மக்கள்",
"sidebar": "பக்கப்பட்டி",
"clickHere": "இங்கே திட்டமிடு", "clickHere": "இங்கே திட்டமிடு",
"minimize": "சுருக்கப்படுதல்", "minimize": "சுருக்கப்படுதல்",
"moveAside": "பக்கத்திற்கு நீக்கு", "moveAside": "பக்கத்திற்கு நீக்கு",
......
{ {
"openInPip": "కొపీలోట్ విండోలో తెరలంచండి", "openInPip": "పాపప్‌లో తెరవండి",
"other": "ఇతర", "openInSidebar": "సైడ్‌బార్‌లో తెరవండి",
"popular": "జనాదరణ పొందింది",
"sidebar": "సైడ్‌బార్",
"clickHere": "ఇక్కడ రాండండి", "clickHere": "ఇక్కడ రాండండి",
"minimize": "తక్కువ చేయడం", "minimize": "తక్కువ చేయడం",
"moveAside": "చేరుకుందండి", "moveAside": "చేరుకుందండి",
......
{ {
"openInPip": "เปิดในหน้าต่าง Copilot", "openInPip": "เปิดในป๊อปอัป",
"other": "อื่น ๆ", "openInSidebar": "เปิดในแถบด้านข้าง",
"popular": "เป็นที่นิยม",
"sidebar": "แถบด้านข้าง",
"clickHere": "คลิกที่นี่", "clickHere": "คลิกที่นี่",
"minimize": "ย่อ", "minimize": "ย่อ",
"moveAside": "เลื่อนข้างหลัง", "moveAside": "เลื่อนข้างหลัง",
......
{ {
"openInPip": "Copilot penceresinde aç", "openInPip": "Açılır pencerede açık",
"other": "Diğer", "openInSidebar": "Kenar çubuğunda açık",
"popular": "Popüler",
"sidebar": "Kenar çubuğu",
"clickHere": "Buraya tıklayın", "clickHere": "Buraya tıklayın",
"minimize": "Küçült", "minimize": "Küçült",
"moveAside": "Kenara Taşı", "moveAside": "Kenara Taşı",
......
{ {
"openInPip": "Відкрити у вікні Copilot", "openInPip": "Відкрито у спливаючому віці",
"other": "Інше", "openInSidebar": "Відкрито на бічній панелі",
"popular": "Популярний",
"sidebar": "Бічна панель",
"clickHere": "Натисніть тут", "clickHere": "Натисніть тут",
"minimize": "Мінімізувати", "minimize": "Мінімізувати",
"moveAside": "Відкрити сторонній", "moveAside": "Відкрити сторонній",
......
{ {
"openInPip": "Mở trong cửa sổ Copilot", "openInPip": "Mở cửa bật lên",
"other": "Khác", "openInSidebar": "Mở trong thanh bên",
"popular": "Phổ biến",
"sidebar": "Thanh bên",
"clickHere": "Nhấp vào đây", "clickHere": "Nhấp vào đây",
"minimize": "Thu nhỏ", "minimize": "Thu nhỏ",
"moveAside": "Di chuyển sang một bên", "moveAside": "Di chuyển sang một bên",
......
{ {
"openInPip": "在 Copilot 窗口中打开", "openInPip": "在弹出窗口中打开",
"other": "其他", "openInSidebar": "在侧边栏中打开",
"popular": "受欢迎的",
"sidebar": "侧边栏",
"clickHere": "点击这里", "clickHere": "点击这里",
"minimize": "最小化", "minimize": "最小化",
"moveAside": "移到旁边", "moveAside": "移到旁边",
......
{ {
"openInPip": "在 Copilot 窗口中打開", "openInPip": "在彈出窗口中打開",
"other": "其他", "openInSidebar": "在側邊欄中打開",
"popular": "受歡迎的",
"sidebar": "側邊欄",
"clickHere": "點擊這裡", "clickHere": "點擊這裡",
"minimize": "最小化", "minimize": "最小化",
"moveAside": "移到旁邊", "moveAside": "移到旁邊",
......
import { ContentScriptId } from "./types"
const __DEV__ = process.env.NODE_ENV == "development" const __DEV__ = process.env.NODE_ENV == "development"
const contentCss = "/assets/index.css" /** Manually register in the service worker */
export const mainContentScript = {
id: ContentScriptId.main,
js: ["js/content-main.js"],
runAt: "document_start",
world: "MAIN",
matches: ["<all_urls>"],
} satisfies chrome.scripting.RegisteredContentScript
export const allFrameScript = {
id: ContentScriptId.frame,
js: ["js/content-frame.js"],
allFrames: true,
runAt: "document_start",
matches: ["<all_urls>"],
} satisfies chrome.scripting.RegisteredContentScript
export const defaultSidebarPath = "sidebar.html"
const manifest = { const manifest = {
manifest_version: 3, manifest_version: 3,
...@@ -10,7 +29,7 @@ const manifest = { ...@@ -10,7 +29,7 @@ const manifest = {
// short_name: "__MSG_short_name__", // short_name: "__MSG_short_name__",
// no more than 132 characters // no more than 132 characters
description: "__MSG_description__", description: "__MSG_description__",
version: "1.2.1", version: "1.2.3",
action: { action: {
default_icon: { default_icon: {
16: "logo.png", 16: "logo.png",
...@@ -18,7 +37,7 @@ const manifest = { ...@@ -18,7 +37,7 @@ const manifest = {
32: "logo.png", 32: "logo.png",
}, },
default_title: "__MSG_short_name__", default_title: "__MSG_short_name__",
default_popup: "src/pages/popup.html", default_popup: "popup.html",
}, },
default_locale: "en", default_locale: "en",
icons: { icons: {
...@@ -27,26 +46,40 @@ const manifest = { ...@@ -27,26 +46,40 @@ const manifest = {
48: "logo.png", 48: "logo.png",
128: "logo.png", 128: "logo.png",
}, },
author: "support@ziziyi.com", author: { email: "support@ziziyi.com" },
background: { background: {
service_worker: "bg.js", service_worker: "./src/bg/index.ts",
type: "module", type: "module" as const,
}, },
content_scripts: [ content_scripts: [
// {
// matches: ["<all_urls>"],
// js: ["src/content/main.ts"],
// run_at: "document_start",
// world: "MAIN",
// },
{ {
matches: ["<all_urls>"], matches: ["http://placeholder.ziziyi.com/*"],
js: ["/js/content-main.js"], js: ["src/content/index.ts"],
run_at: "document_start", run_at: "document_start",
world: "MAIN",
}, },
{ ],
matches: ["<all_urls>"], options_page: __DEV__ ? "sidebar.html" : undefined,
js: ["/js/content.js"], side_panel: {
run_at: "document_start", default_path: defaultSidebarPath,
}, },
permissions: [
"tabs",
"scripting",
"activeTab",
"storage",
"offscreen",
"sidePanel",
"declarativeNetRequestWithHostAccess",
"declarativeNetRequestFeedback",
"webNavigation",
], ],
options_page: __DEV__ ? "/src/pages/guide.html" : "", optional_permissions: [],
permissions: ["tabs", "scripting", "activeTab", "storage", "offscreen"],
host_permissions: ["<all_urls>"], host_permissions: ["<all_urls>"],
minimum_chrome_version: "111", minimum_chrome_version: "111",
commands: { commands: {
...@@ -60,8 +93,13 @@ const manifest = { ...@@ -60,8 +93,13 @@ const manifest = {
}, },
web_accessible_resources: [ web_accessible_resources: [
{ {
resources: [contentCss, "logo.svg"], resources: ["logo.svg"],
matches: ["<all_urls>"],
},
{
resources: ["/js/*", "/assets/*"],
matches: ["<all_urls>"], matches: ["<all_urls>"],
use_dynamic_url: true,
}, },
], ],
content_security_policy: { content_security_policy: {
...@@ -69,6 +107,6 @@ const manifest = { ...@@ -69,6 +107,6 @@ const manifest = {
? `script-src 'self' http://localhost:3000 'wasm-unsafe-eval';` ? `script-src 'self' http://localhost:3000 'wasm-unsafe-eval';`
: `script-src 'self' 'wasm-unsafe-eval'`, : `script-src 'self' 'wasm-unsafe-eval'`,
}, },
} } satisfies Manifest as chrome.runtime.Manifest
export default manifest export default manifest
This diff is collapsed.
<script setup lang="ts">
import { ref, onMounted, reactive, computed } from "vue"
import { getLocal, updateFrameNetRules } from "@/utils/ext"
import config from "@/assets/config.json"
import LoadingBar from "@/components/LoadingBar.vue"
import Webview from "@/components/Webview.vue"
import { useI18n } from "@/utils/i18n"
const logoUrl = chrome.runtime.getURL("/logo.svg")
const { t } = useI18n()
const url = ref("")
const popularItems = reactive(config.data.popularSites)
const recentItems = reactive<{ url: string; title: string; icon: string }[]>([])
const protectedUrl = computed(() => {
if (!url.value) {
return true
}
const u = new URL(url.value)
if (!["http:", "https:"].includes(u.protocol)) {
return true
}
return false
})
onMounted(() => {
const q = new URLSearchParams(location.search)
const initUrl = q.get("url") || ""
url.value = initUrl
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 go(link: string) {
url.value = link
}
</script>
<template>
<div class="w-full h-screen">
<Webview v-if="!protectedUrl" :url="url" />
<div v-else class="flex flex-col p-6 max-w-md mx-auto">
<div class="flex flex-col items-center gap-2 mx-auto mt-16">
<img :src="logoUrl" class="size-16" />
<span class="text-2xl font-bold my-2">{{ t("sidebar") }}</span>
</div>
<div class="grid grid-cols-4 gap-y-4 justify-between mt-24">
<button
v-for="item of recentItems"
class="group w-16 shrink-0 relative flex flex-col items-center justify-between justify-self-center rounded-lg px-2 py-3 bg-background-soft"
@click="go(item.url)"
>
<div
class="size-6 rounded"
:style="{
background: 'center / contain url(' + item.icon + ')',
}"
></div>
<div
class="text-xs max-w-full mt-1 break-words leading-3 line-clamp-2"
>
{{ item.title }}
</div>
</button>
</div>
<!-- <div class="text-center my-3">Popular</div> -->
<div class="w-full my-6 border-b border-b-slate-400/60 h-0"></div>
<div class="grid grid-cols-4 gap-y-4 justify-between">
<button
v-for="item of popularItems"
class="group w-16 shrink-0 relative flex flex-col items-center justify-between justify-self-center rounded-lg px-2 py-3 bg-background-soft"
@click="go(item.url)"
>
<div
class="size-6 rounded"
:style="{
background: 'center / contain url(' + item.icon + ')',
}"
></div>
<div
class="text-xs max-w-full mt-1 break-words leading-3 line-clamp-2"
>
{{ item.title }}
</div>
</button>
</div>
</div>
</div>
<!-- <LoadingBar /> -->
</template>
<style scoped></style>
import "@/content/index" import "@/content/index"
import "@/pages/popup" import "@/pages/popup"
import { testFirebase } from "@/utils/firebase"
testFirebase()
import "@/assets/main.css"
import { createApp } from "vue"
import { i18n } from "@/utils/i18n"
import Sidebar from "./Sidebar.vue"
const app = createApp(Sidebar)
app.use(i18n)
app.mount("#app")
import { reactive, ref } from "vue" import { reactive, ref } from "vue"
/** popup state */
export const items = reactive([
{
url: "https://chat.openai.com/",
img: "/img/chatgpt.svg",
title: "ChatGPT - OpenAI",
},
{
url: "https://bard.google.com/",
img: "/img/bard.svg",
title: "Bard - Google AI",
},
{
url: "https://claude.ai/",
img: "/img/claude-ai.svg",
title: "Claude",
},
{
url: "https://tiktok.com/",
img: "/img/tiktok.svg",
title: "Tiktok",
},
])
export const pipLauncher = reactive({ export const pipLauncher = reactive({
visible: false, visible: false,
}) })
......
declare namespace chrome.declarativeNetRequest {
export function getSessionRules(filter: {
ruleIds: number[]
}): Promise<Rule[]>
}
declare interface Manifest extends chrome.runtime.ManifestV3 {
web_accessible_resources: Array<{
resources: string[]
matches: string[]
use_dynamic_url?: boolean
}>
}
...@@ -16,6 +16,7 @@ export enum MessageType { ...@@ -16,6 +16,7 @@ export enum MessageType {
invokeRequest = "invoke-request", invokeRequest = "invoke-request",
invokeResponse = "invoke-Response", invokeResponse = "invoke-Response",
showChatDocs = "show-chat-docs", showChatDocs = "show-chat-docs",
frameReady = "frame-ready",
} }
export enum ServiceFunc { export enum ServiceFunc {
...@@ -31,3 +32,23 @@ export type ParseDocOptions = { ...@@ -31,3 +32,23 @@ export type ParseDocOptions = {
size: number size: number
url: string url: string
} }
export enum ContentScriptId {
content = "content",
main = "content-main",
frame = "frame",
}
export enum ContentEventType {
pip = "anything-copilot_pip",
pipLoad = "anything-copilot_pip-load",
pipLoaded = "anything-copilot_pip-loaded",
escapeLoad = "anything-copilot_escape-load",
}
export enum FrameMessageType {
frameReady = "anything-copilot_frame-ready",
contentRun = "anything-copilot_content-run",
escapeLoad = "anything-copilot_escape-load",
pageInfo = "anything-copilot_page-info",
}
...@@ -215,3 +215,21 @@ export async function waitFor( ...@@ -215,3 +215,21 @@ export async function waitFor(
} }
} }
} }
export function getPageIcon() {
const icons = document.querySelectorAll<HTMLLinkElement>('link[rel="icon"]')
for (let item of icons) {
if (item.getAttribute("type") == "image/svg+xml") {
return item.href
}
if (parseInt(item.getAttribute("sizes") || "0") >= 90) {
return item.href
}
}
if (icons.length) {
return icons[0].href
}
return location.origin + "/favicon.ico"
}
import { MessageType } from "@/types"; import { mainContentScript } from "@/manifest"
import { MessageType } from "@/types"
type MessageSender = chrome.runtime.MessageSender; type MessageSender = chrome.runtime.MessageSender
type UpdatedOption = { type UpdatedOption = {
tabId: number; tabId: number
status: string; status: string
timeout?: number; timeout?: number
}; }
export async function tabUpdated({ tabId, status, timeout }: UpdatedOption) { export async function tabUpdated({ tabId, status, timeout }: UpdatedOption) {
return new Promise<void>((r) => { return new Promise<void>((r) => {
const handleUpdate = (id: number, info: chrome.tabs.TabChangeInfo) => { const handleUpdate = (id: number, info: chrome.tabs.TabChangeInfo) => {
console.log(id, info); console.log(id, info)
if (id === tabId && info.status === status) { if (id === tabId && info.status === status) {
chrome.tabs.onUpdated.removeListener(handleUpdate); chrome.tabs.onUpdated.removeListener(handleUpdate)
r(); r()
}
} }
};
setTimeout(() => { setTimeout(() => {
chrome.tabs.onUpdated.removeListener(handleUpdate); chrome.tabs.onUpdated.removeListener(handleUpdate)
r(); r()
}, timeout || 30 * 1000); }, timeout || 30 * 1000)
chrome.tabs.onUpdated.addListener(handleUpdate); chrome.tabs.onUpdated.addListener(handleUpdate)
}); })
} }
type WaitMessageOption = { type WaitMessageOption = {
type: string; type: string
tabId?: number; tabId?: number
timeout?: number; timeout?: number
}; }
export async function waitMessage<T = unknown>({ export async function waitMessage<T = unknown>({
type, type,
...@@ -37,23 +38,23 @@ export async function waitMessage<T = unknown>({ ...@@ -37,23 +38,23 @@ export async function waitMessage<T = unknown>({
timeout, timeout,
}: WaitMessageOption): Promise<T> { }: WaitMessageOption): Promise<T> {
return new Promise<T>((r, reject) => { return new Promise<T>((r, reject) => {
let timer = 0; let timer = 0
const handleMessage = (message: any, sender: MessageSender) => { const handleMessage = (message: any, sender: MessageSender) => {
const sameId = typeof tabId == "number" ? tabId == sender.tab?.id : true; const sameId = typeof tabId == "number" ? tabId == sender.tab?.id : true
if (sameId && message.type == type) { if (sameId && message.type == type) {
chrome.runtime.onMessage.removeListener(handleMessage); chrome.runtime.onMessage.removeListener(handleMessage)
r(message); r(message)
clearTimeout(timer); clearTimeout(timer)
}
} }
}; chrome.runtime.onMessage.addListener(handleMessage)
chrome.runtime.onMessage.addListener(handleMessage);
if (timeout) { if (timeout) {
timer = window.setTimeout(() => { timer = window.setTimeout(() => {
chrome.runtime.onMessage.removeListener(handleMessage); chrome.runtime.onMessage.removeListener(handleMessage)
reject(); reject()
}, timeout); }, timeout)
} }
}); })
} }
export const emptyTab: chrome.tabs.Tab = { export const emptyTab: chrome.tabs.Tab = {
...@@ -69,70 +70,71 @@ export const emptyTab: chrome.tabs.Tab = { ...@@ -69,70 +70,71 @@ export const emptyTab: chrome.tabs.Tab = {
index: 0, index: 0,
pinned: false, pinned: false,
windowId: 0, windowId: 0,
}; }
export async function checkContent(tabId: number) { export async function checkContent(tabId: number) {
let alive = false; let alive = false
try { try {
const resMsgPromise = new Promise<void>((r) => { const resMsgPromise = new Promise<void>((r) => {
const handleMessage = (message: any, sender: MessageSender) => { const handleMessage = (message: any, sender: MessageSender) => {
if (sender.tab?.id == tabId) { if (sender.tab?.id == tabId) {
alive = true; alive = true
r(); r()
chrome.runtime.onMessage.removeListener(handleMessage); chrome.runtime.onMessage.removeListener(handleMessage)
}
} }
}; setTimeout(() => r(), 3000)
setTimeout(() => r(), 3000); chrome.runtime.onMessage.addListener(handleMessage)
chrome.runtime.onMessage.addListener(handleMessage); })
});
const res = await chrome.tabs.sendMessage(tabId, { const res = await chrome.tabs.sendMessage(tabId, {
type: MessageType.hiContent, type: MessageType.hiContent,
}); })
alive = !!res; alive = !!res
console.log("hi-content response: ", alive, res); console.log("hi-content response: ", alive, res)
if (!alive) { if (!alive) {
await resMsgPromise; await resMsgPromise
} }
} catch (err) { } catch (err) {
console.warn(err); console.warn(err)
console.log("content is not available") console.log("content is not available")
} }
console.log("checkContent alive: ", alive); console.log("checkContent alive: ", alive)
if (alive) { if (alive) {
return true; return true
} }
const manifest = chrome.runtime.getManifest(); const manifest = chrome.runtime.getManifest()
if (!manifest.content_scripts) { if (!manifest.content_scripts) {
return false; return false
} }
const contentScripts = [...manifest.content_scripts, mainContentScript]
try { try {
for (let item of manifest.content_scripts) { for (let item of contentScripts) {
if (item.js) { if (item.js) {
const world = const world =
"world" in item && item.world == "MAIN" ? "MAIN" : "ISOLATED"; "world" in item && item.world == "MAIN" ? "MAIN" : "ISOLATED"
await chrome.scripting.executeScript({ await chrome.scripting.executeScript({
files: item.js, files: item.js,
target: { tabId: tabId }, target: { tabId: tabId },
world: world, world: world,
}); })
} }
} }
return true; return true
} catch (e) { } catch (e) {
return false; return false
} }
} }
type StoreUrlOptions = { type StoreUrlOptions = {
id: string id: string
name: string name: string
...@@ -174,10 +176,64 @@ export function getStoreUrl(options: StoreUrlOptions) { ...@@ -174,10 +176,64 @@ export function getStoreUrl(options: StoreUrlOptions) {
}) })
} }
export function getLocal<T extends Record<string, any>>(key: string | T,) { export function getLocal<T extends Record<string, any>>(key: string | T) {
return chrome.storage.local.get(key) as Promise<T> return chrome.storage.local.get(key) as Promise<T>
} }
export function getSession<T extends Record<string, any>>(key: string | T) { export function getSession<T extends Record<string, any>>(key: string | T) {
return chrome.storage.session.get(key) as Promise<T> return chrome.storage.session.get(key) as Promise<T>
} }
type NetRulesOptions = {
ua?: string
tabIds?: number[]
initiatorDomains?: string[]
}
export async function updateFrameNetRules(
{ ua, tabIds, initiatorDomains }: NetRulesOptions = {
tabIds: [-1],
initiatorDomains: [chrome.runtime.id],
}
) {
ua = ua || navigator.userAgent
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,
},
{
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,
tabIds,
},
},
],
})
}
import { initializeApp } from "firebase/app"
import {
activate,
fetchAndActivate,
getRemoteConfig,
getValue,
isSupported,
} from "firebase/remote-config"
// TODO: Replace the following with your app's Firebase project configuration
// See: https://firebase.google.com/docs/web/learn-more#config-object
const firebaseConfig = {
// The value of `databaseURL` depends on the location of the database
// databaseURL: "https://DATABASE_NAME.firebaseio.com",
// For Firebase JavaScript SDK v7.20.0 and later, `measurementId` is an optional field
// measurementId: "G-MEASUREMENT_ID",
apiKey: "AIzaSyBkNIquKSxOfJxZErQtlIr--Ae-c4ZXZzg",
authDomain: "anything-copilot.firebaseapp.com",
projectId: "anything-copilot",
storageBucket: "anything-copilot.appspot.com",
messagingSenderId: "303124265017",
appId: "1:303124265017:web:92ce306c269fde39e175e8",
}
// Initialize Firebase
export const app = initializeApp(firebaseConfig)
// Initialize Remote Config and get a reference to the service
export const remoteConfig = getRemoteConfig(app)
remoteConfig.settings.minimumFetchIntervalMillis = 3600000
export async function testFirebase() {
const supported = await isSupported()
const fetched = await fetchAndActivate(remoteConfig)
const activated = await activate(remoteConfig)
console.log("fetchAndActivate", supported, fetched, activated)
const a = getValue(remoteConfig, "tmp_test")
console.log(a)
}
...@@ -7,7 +7,7 @@ type MessageSchema = typeof EnMessage & typeof ZhMessage ...@@ -7,7 +7,7 @@ type MessageSchema = typeof EnMessage & typeof ZhMessage
export function getLocale() { export function getLocale() {
if (__DEV__) { if (__DEV__) {
return "en" return "zh-CN"
} }
const language = chrome.i18n.getUILanguage() const language = chrome.i18n.getUILanguage()
......
...@@ -50,3 +50,39 @@ export const semanticClip = (text: string, maxLength: number) => { ...@@ -50,3 +50,39 @@ export const semanticClip = (text: string, maxLength: number) => {
return text.slice(0, breakPoint) return text.slice(0, breakPoint)
} }
export async function findFrameLoadUrl(urls: string[]) {
const abortController = new AbortController()
let resolve: null | ((url: string) => void) = null
const value = new Promise<string>((r) => {
resolve = r
})
function checkCSP(csp?: string | null) {
if (!csp) return true
return !csp.includes("frame-ancestors")
}
const promises = urls.map((url) =>
fetch(url, {
signal: abortController.signal,
})
.then((res) => {
const h = res.headers
const xFrameOptions = h.get("X-Frame-Options")
const csp = h.get("Content-Security-Policy")
if (!xFrameOptions && checkCSP(csp)) {
resolve && resolve(url)
abortController.abort()
}
})
.catch(() => {})
)
Promise.all(promises).then(() => {
resolve && resolve("")
})
return value
}
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { export default {
content: ["./src/**/*.{vue,js,ts,jsx,tsx}"], content: ["./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: { theme: {
borderRadius: { borderRadius: {
...@@ -14,22 +14,10 @@ module.exports = { ...@@ -14,22 +14,10 @@ module.exports = {
full: "9999px", full: "9999px",
}, },
fontSize: { fontSize: {
// xs: "12px",
// sm: "14px",
// base: "16px",
// lg: "18px",
// xl: "20px",
// "2xl": "24px",
// "3xl": "30px",
// "4xl": "36px",
// "5xl": "48px",
// "6xl": "60px",
// "7xl": "72px",
xs: ["12px", { lineHeight: "16px" }], xs: ["12px", { lineHeight: "16px" }],
sm: ["14px", { lineHeight: "20px" }], sm: ["14px", { lineHeight: "20px" }],
base: ["16px", { lineHeight: "24px" }], base: ["16px", { lineHeight: "24px" }],
lg: ["18.px", { lineHeight: "28px" }], lg: ["18px", { lineHeight: "28px" }],
xl: ["20px", { lineHeight: "28px" }], xl: ["20px", { lineHeight: "28px" }],
"2xl": ["24px", { lineHeight: "32px" }], "2xl": ["24px", { lineHeight: "32px" }],
"3xl": ["30px", { lineHeight: "36px" }], "3xl": ["30px", { lineHeight: "36px" }],
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
"src/**/*.json", "src/**/*.json",
"tests/**/*" "tests/**/*"
], ],
"exclude": ["src/**/__tests__/*", "src/manifest.ts"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"baseUrl": ".", "baseUrl": ".",
......
import click
from os import path, listdir
import json
import pandas as pd
@click.group()
@click.option('-d', default='src/locales', help="locales directory path")
@click.option('--filename', default='messages.json', help="locale message filename")
@click.pass_context
def cli(ctx, d: str, filename: str):
locales_dir = path.realpath(path.join(d))
dir_items = listdir(locales_dir)
def get_path(p: str):
msg_path = path.join(locales_dir,p)
return path.join(msg_path, filename) if path.isdir(msg_path) else msg_path
items = { path.splitext(i)[0]:get_path(i) for i in dir_items}
ctx.obj['locales_dir'] = locales_dir
ctx.obj['items'] = items
# extract i18n to csv
@cli.command()
@click.option('-p','--preference', default=['en', 'zh-CN'], multiple=True, help="preference languages")
@click.option('-o', '--output', default='-', help="output")
@click.pass_context
def extract(ctx, preference:list, output:str):
items = ctx.obj['items']
msgs = {
code: json.load(open(items[code], 'r', encoding='utf8'))
for code in items.keys()
}
series_list =[
pd.json_normalize(v).rename({ 0: k }).transpose()[k]
for k, v in msgs.items()
]
df = pd.DataFrame(series_list).transpose()
code_list = [*preference, *[code for code in msgs.keys() if code not in preference]]
df = df[code_list]
if output == '-':
print(df.to_csv())
else:
df.to_csv(output)
@cli.command()
@click.option('-t', default='', help='')
@click.option('-i', '--increment', default=True, help='Incremental update')
@click.pass_context
def update(ctx, t: str, increment: bool):
locales_dir = ctx.obj['locales_dir']
translated_path = path.realpath(t)
def merge(d, d2):
n = {**d}
for k, v in d2.items():
n[k] = v if type(v) == str else merge(n[k], v)
return n
def update_msg(code, content):
filename = code.replace('_', '-')
msg_path = path.join(locales_dir, f'{filename}.json')
try:
msg = json.load(open(msg_path, 'r', encoding='utf8'))
data = merge(msg, content) if increment else content
json.dump(
data,
open(msg_path, 'w+', encoding='utf8'),
ensure_ascii=False,
indent=2
)
except Exception as e:
print('code: ', code)
print(e)
if t.endswith('.jl'):
with open(translated_path, 'r', encoding='utf8') as f:
for line in f:
line_data = json.loads(line)
code = line_data['code']
content = line_data['content']
content = json.loads(content) if type(content) == str else content
update_msg(code, content)
if t.endswith('.json'):
df_dict = json.load(open(translated_path, 'r', encoding='utf8'))
for code in df_dict:
update_msg(code, df_dict[code])
if t.endswith('.csv'):
df = pd.read_csv(translated_path, index_col=0)
df_dict = df.to_dict(orient='dict')
data = {}
for code, d in df_dict.items():
data[code] = {}
for k,v in d.items():
keys = k.split('.')
current = data[code]
for i,key in enumerate(keys):
if i == len(keys) - 1:
current[key] = v
pass
else:
current[key] = current[key] if key in current else {}
current = current[key]
for code in data:
update_msg(code, data[code])
if __name__ == '__main__':
cli(obj={})
#! /usr/bin/env python
import click
import os
import json
import time
import re
import requests
from io import StringIO
import pandas as pd
@click.command()
@click.argument(
"i18n_dir",
type=click.Path(exists=True, dir_okay=True, file_okay=False),
)
@click.argument("csv_path", type=click.Path())
@click.option(
"-e",
"--extract",
default=False,
is_flag=True,
help="extract csv from i18n directory",
)
@click.option("-n", "--msg-name", default="", help="i18n message filename")
@click.option(
"-p",
"--prioritize",
default=["en"],
multiple=True,
help="Languages to prioritize at the beginning of CSV columns, e.g., 'en,zh'.",
)
@click.option(
"-o",
"--overwrite",
default=False,
is_flag=True,
help="overwrites existing i18n messages, default is False",
)
@click.option(
"-w",
"--watch",
default=0.0,
is_flag=False,
flag_value=1,
help="Enable watch mode to automatically update i18n when the CSV file changes.",
)
@click.option("--sheet-name", default=None, help="The name of the sheet")
@click.option(
"--range", default=None, help="Any valid range specifier, e.g. A1:C99 or B2:F"
)
def cli(
i18n_dir: str,
csv_path: str,
extract=False,
msg_name="",
prioritize="",
overwrite=False,
watch=0,
sheet_name=None,
range=None,
):
locales_data = parse_locales(i18n_dir, msg_name)
if extract:
return extract_csv(
locales_data["locales"],
csv_path,
prioritize,
)
count = 0
translated = csv_path
last_hash = 0
while True:
if watch < 0.1:
if count >= 1:
break
elif count > 0:
time.sleep(watch)
count += 1
data, current_hash = parse_translated(
translated, sheet_name=sheet_name, range=range
)
if current_hash == last_hash:
continue
last_hash = current_hash
for code in data:
filename = locales_data["msg_name"].replace("<code>", code)
msg_path = os.path.join(i18n_dir, filename)
update_msg(msg_path, data[code], overwrite=overwrite)
print(f'{time.strftime("%H:%M:%S")} UPDATED hash={current_hash}')
def parse_locales(i18n_dir: str, msg_name: str):
dir_items = os.listdir(i18n_dir)
codes = [os.path.splitext(i)[0] for i in dir_items]
if len(dir_items) > 1 and not msg_name:
if os.path.isfile(os.path.join(i18n_dir, dir_items[0])):
msg_name = "<code>" + os.path.splitext(dir_items[0])[1]
code_dir = os.path.join(i18n_dir, codes[0])
if (not msg_name) and os.path.isdir(code_dir):
items = os.listdir(code_dir)
if len(items) == 1 and not os.path.isdir(items[0]):
msg_name = f"<code>/{items[0]}"
if not msg_name:
msg_name = "<code>.json"
locales = {c: os.path.join(i18n_dir, msg_name.replace("<code>", c)) for c in codes}
return {
"locales": locales,
"msg_name": msg_name,
}
def get_path(i18n_dir: str, item: str, filename: str):
"""parse path from listdir() or code and filename"""
msg_path = os.path.join(i18n_dir, item)
return os.path.join(msg_path, filename) if os.path.isdir(msg_path) else msg_path
def extract_csv(locales: dict, output: str, prioritize: list[str]):
data = {}
for code in list(dict.fromkeys([*prioritize, *locales.keys()])):
with open(locales[code], "r", encoding="utf8") as f:
data[code] = pd.json_normalize(json.load(f)).transpose()[0]
df = pd.DataFrame(data)
if output == "-":
print(df.to_csv())
else:
df.to_csv(output)
def merge(base: dict, additional: dict):
new = {**base, **additional}
for k, v in additional.items():
if isinstance(v, dict) and isinstance(new[k], dict):
new[k] = merge(new[k], v)
return new
def denormalize(value: dict):
data = {}
for code, d in value.items():
data[code] = {}
for k, v in d.items():
keys = k.split(".")
current = data[code]
for i, key in enumerate(keys):
if i == len(keys) - 1:
current[key] = v
pass
else:
current[key] = current[key] if key in current else {}
current = current[key]
return data
def update_msg(msg_path: str, content: dict, overwrite=False):
try:
dir_path = os.path.dirname(msg_path)
os.makedirs(dir_path, exist_ok=True)
open_mode = "r+" if os.path.exists(msg_path) else "w+"
with open(msg_path, open_mode, encoding="utf8") as f:
msg = json.loads(f.read() or "{}")
data = merge(msg, content) if not overwrite else content
f.seek(0)
json.dump(data, f, ensure_ascii=False, indent=2)
f.truncate()
except Exception as e:
print("Error: ", msg_path)
raise e
def parse_translated(translated: str, sheet_name="", range="", index_col=0):
data = {}
sheets_match = re.match(
r"https?://docs.google.com/spreadsheets/d/(.+)/", translated
)
if sheets_match:
sheets_id = sheets_match[1]
qs = ["tqx=out:csv"]
if sheet_name:
qs.append(f"sheet={sheet_name}")
if range:
qs.append(f"range={range}")
res = requests.get(
f'https://docs.google.com/spreadsheets/d/{sheets_id}/gviz/tq?{"&".join(qs)}'
)
df = pd.read_csv(StringIO(res.text), index_col=index_col).fillna("")
return denormalize(df.to_dict(orient="dict")), hash(res.text)
csv_url_match = re.match(r"https?://.+\.csv(?![^?#])", translated)
if csv_url_match:
res = requests.get(translated)
df = pd.read_csv(StringIO(res.text), index_col=index_col).fillna("")
return denormalize(df.to_dict(orient="dict")), hash(res.text)
mtime = os.stat(translated).st_mtime
if translated.endswith(".csv"):
df = pd.read_csv(translated, index_col=index_col).fillna("")
return denormalize(df.to_dict(orient="dict")), mtime
if translated.endswith(".json"):
with open(translated, "r", encoding="utf8") as f:
data = json.load(f)
return data, mtime
if translated.endswith(".jl"):
with open(translated, "r", encoding="utf8") as f:
for line in f:
data = {**data, **json.load(line)}
return data, mtime
if __name__ == "__main__":
cli()
\ No newline at end of file
...@@ -7,11 +7,15 @@ import vueJsx from "@vitejs/plugin-vue-jsx" ...@@ -7,11 +7,15 @@ import vueJsx from "@vitejs/plugin-vue-jsx"
import vueI18n from "@intlify/unplugin-vue-i18n/vite" import vueI18n from "@intlify/unplugin-vue-i18n/vite"
import wasm from "vite-plugin-wasm" import wasm from "vite-plugin-wasm"
import topLevelAwait from "vite-plugin-top-level-await" import topLevelAwait from "vite-plugin-top-level-await"
import { crx } from "@crxjs/vite-plugin"
import manifest from "./src/manifest" import manifest from "./src/manifest"
import makeManifest from "./utils/manifest-plugin" import copy from "rollup-plugin-copy"
// import makeManifest from "./utils/manifest-plugin"
/// <reference types="vitest" /> /// <reference types="vitest" />
const __DEV__ = process.env.NODE_ENV == "development"
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
define: { define: {
...@@ -21,7 +25,11 @@ export default defineConfig({ ...@@ -21,7 +25,11 @@ export default defineConfig({
}, },
plugins: [ plugins: [
vue(), copy({
hook: "buildEnd",
targets: __DEV__ ? [{ src: "public/*", dest: "dist" }] : [],
}),
vue({}),
vueJsx(), vueJsx(),
vueI18n({ vueI18n({
runtimeOnly: true, runtimeOnly: true,
...@@ -30,32 +38,32 @@ export default defineConfig({ ...@@ -30,32 +38,32 @@ export default defineConfig({
"./src/locales/**" "./src/locales/**"
), ),
}), }),
makeManifest(manifest, { isDev: false }), crx({ manifest, contentScripts: { injectCss: false } }),
wasm(), wasm(),
topLevelAwait(), topLevelAwait(),
], ],
resolve: { resolve: {
alias: { alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)), "@": fileURLToPath(new URL("./src", import.meta.url)),
"vue-i18n": "vue-i18n/dist/vue-i18n.runtime.esm-bundler.js", "vue-i18n": resolve(
__dirname,
"node_modules",
"vue-i18n/dist/vue-i18n.runtime.esm-bundler.js"
),
}, },
}, },
build: { build: {
target: ["chrome111"], target: ["chrome111"],
emptyOutDir: false, emptyOutDir: true,
assetsDir: "assets", // cssCodeSplit: false,
outDir: "dist", outDir: "dist",
rollupOptions: { rollupOptions: {
input: { input: {
popup: "src/pages/popup.html", offscreen: "offscreen.html",
guide: "src/pages/guide.html",
worker: "src/pages/offscreen.html",
// dev: "src/pages/dev.html",
}, },
output: { output: {
assetFileNames: "assets/[name].[ext]", chunkFileNames: "js/chunk-[hash].js",
chunkFileNames: "js/[name]-chunk.js", assetFileNames: "assets/[name][extname]",
entryFileNames: "js/[name].js",
}, },
}, },
}, },
......
import { defineConfig } from "vite"
import { fileURLToPath, URL } from "node:url"
import { dirname, resolve } from "node:path"
import vue from "@vitejs/plugin-vue"
import vueJsx from "@vitejs/plugin-vue-jsx"
import vueI18n from "@intlify/unplugin-vue-i18n/vite"
export default defineConfig({
server: {
port: 3000,
},
define: {
__INTLIFY_JIT_COMPILATION__: true,
__INTLIFY_DROP_MESSAGE_COMPILER__: true,
__DEV__: process.env.NODE_ENV === "development",
},
plugins: [
vue(),
vueJsx(),
vueI18n({
runtimeOnly: true,
include: resolve(
dirname(fileURLToPath(import.meta.url)),
"./src/locales/**"
),
}),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
"vue-i18n": "vue-i18n/dist/vue-i18n.runtime.esm-bundler.js",
},
},
build: {
copyPublicDir: false,
emptyOutDir: false,
rollupOptions: {
input: {
content: "./src/content/index.ts",
},
output: {
assetFileNames: "assets/[name].[ext]",
chunkFileNames: "js/[name]-chunk.js",
entryFileNames: "js/[name].js",
},
},
},
})
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