Commit 1d61640e authored by Domi's avatar Domi

feat: chrome sidebar

parent dd609683
......@@ -13,6 +13,7 @@ dist
dist-ssr
coverage
*.local
public/js
/cypress/videos/
/cypress/screenshots/
......
......@@ -9,7 +9,6 @@
</head>
<body>
<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>
</html>
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({
absWorkingDir: resolve(dirname(fileURLToPath(import.meta.url))),
entryPoints: {
bg: "./src/bg/index.ts",
// bg: "./src/bg/index.ts",
"js/content-main": "./src/content/main.ts",
"js/content-frame": "./src/content/frame.ts",
"js/pdf.worker": "./src/assets/pdf.worker.js",
},
bundle: true,
format: "iife",
outdir: "./dist",
outdir: "./public",
alias: {
"@": "./src/",
},
......
......@@ -8,6 +8,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="./guide.ts"></script>
<script type="module" src="./src/pages/guide.ts"></script>
</body>
</html>
......@@ -8,6 +8,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="./offscreen.ts"></script>
<script type="module" src="./src/pages/offscreen.ts"></script>
</body>
</html>
This diff is collapsed.
{
"name": "picture-in-picture",
"version": "0.0.0",
"name": "anything-copilot",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "run-p dev:page dev:content dev:js",
"dev:page": "cross-env NODE_ENV=development vite build --watch",
"dev:content": "vite build --watch -c vite.content.config.ts",
"dev:js": "node esbuild.mjs --watch",
"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",
"dev:vite": "vite",
"dev": "run-p dev:vite build:esbuild",
"build": "npm run build:esbuild && vite build",
"build:esbuild": "node esbuild.mjs",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"zip:win": "7z a anything-copilot.zip .\\dist\\*",
"zip": "",
......@@ -36,9 +32,10 @@
"vue-i18n": "^9.7.0"
},
"devDependencies": {
"@crxjs/vite-plugin": "^2.0.0-beta.23",
"@intlify/unplugin-vue-i18n": "^1.5.0",
"@tsconfig/node18": "^18.2.2",
"@types/chrome": "^0.0.251",
"@types/chrome": "^0.0.261",
"@types/lodash-es": "^4.17.11",
"@types/node": "^18.18.5",
"@vitejs/plugin-vue": "^4.4.0",
......@@ -48,6 +45,8 @@
"cross-env": "^7.0.3",
"npm-run-all2": "^6.1.1",
"postcss": "^8.4.31",
"rollup-plugin-copy": "^3.5.0",
"sass": "^1.71.1",
"tailwindcss": "^3.4.1",
"typescript": "~5.2.0",
"vite": "^4.4.11",
......
......@@ -8,6 +8,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="./popup.ts"></script>
<script type="module" src="./src/pages/popup.ts"></script>
</body>
</html>
module.exports = {
export default {
plugins: {
tailwindcss: {},
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 @@
"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,
}
}
@media (min-width: 1024px) {
/* @media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
/* #app {
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
} */
}
}
} */
.scrollbar::-webkit-scrollbar {
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 { 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) {
const tab = await chrome.tabs.create({
......@@ -87,6 +107,7 @@ async function handleInvokeRequest(
break
}
} catch (err) {
console.error("invoke error: ", err)
error = err
}
......@@ -155,18 +176,30 @@ function handleCommand(command: string) {
chrome.commands.onCommand.addListener(handleCommand)
async function updateConfig() {
const url = 'https://config.ziziyi.com/anything-copilot'
const url = "https://config.ziziyi.com/anything-copilot"
const now = Date.now()
let { timestamp, configVersion, chatDocSites } = await getLocal({
let {
timestamp,
configVersion,
chatDocSites,
webviewPatchs,
popularSites,
loadCandidates,
} = await getLocal({
timestamp: {} as Record<string, number>,
configVersion: config.data.configVersion,
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
}
......@@ -177,17 +210,32 @@ async function updateConfig() {
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
}
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
await chrome.storage.local.set({
chatDocSites,
timestamp,
chatDocSites,
webviewPatchs,
popularSites,
loadCandidates,
})
}
chrome.runtime.onStartup.addListener(() => {
updateConfig()
})
\ No newline at end of file
})
......@@ -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 {
public readonly path: string
......@@ -49,7 +49,9 @@ class Offscreen extends Invoke {
public async send(req: any): Promise<{ key: string; response: any }> {
const key = this.key
await this.setup()
console.log("offscreen send: ", key, req)
const response = await chrome.runtime.sendMessage({
type: MessageType.toOffscreen,
key,
......
......@@ -3,7 +3,7 @@ import { ref, computed, watch, onMounted, onUnmounted } from "vue"
import IconMinimize from "@/components/icons/IconMinimize.vue"
import IconSplitRight from "@/components/icons/IconSplitscreenRight.vue"
import IconClose from "@/components/icons/IconClose.vue"
import { MessageType } from "@/types"
import { ContentEventType, MessageType } from "@/types"
import { pipWindow } from "@/store"
import { throttle } from "lodash-es"
import IconRefresh from "./icons/IconRefresh.vue"
......@@ -140,7 +140,7 @@ const refresh = () => {
const win = pipWindow.window
if (win) {
dispatchContentEvent({
type: "load-doc",
type: ContentEventType.pipLoad,
detail: { url: win.location.href },
})
}
......
......@@ -3,6 +3,7 @@ import { dispatchContentEvent } from "@/content/event"
import { reactive, ref } from "vue"
import IconClose from "./icons/IconClose.vue"
import { useI18n } from "@/utils/i18n"
import { ContentEventType } from "@/types"
const { t } = useI18n()
......@@ -12,7 +13,7 @@ const host = ref(location.host)
function handleClick() {
dispatchContentEvent({
type: "pip",
type: ContentEventType.pip,
detail: {
url: location.href,
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>
......@@ -98,14 +98,14 @@ function adjustPosition() {
rect.left < 0
? -rect.left
: rect.right > innerWidth
? innerWidth - rect.right
: 0
? innerWidth - rect.right
: 0
const dy =
rect.top < 0
? -rect.top
: rect.bottom > innerHeight
? innerHeight - rect.bottom
: 0
? innerHeight - rect.bottom
: 0
position.tx += dx
position.ty += dy
// div.style.transform = `translate(${position.tx}px, ${position.ty}px)`
......@@ -125,10 +125,11 @@ onMounted(() => {
const { host, pathname } = doc.location
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) {
siteConfig.maxInput = matchConfig.maxInputToken || matchConfig.maxInputLength
siteConfig.maxInput =
matchConfig.maxInputToken || matchConfig.maxInputLength
siteConfig.maxInputType = matchConfig.maxInputToken ? "token" : "char"
siteConfig.selector = matchConfig.selector
}
......@@ -147,15 +148,21 @@ onUnmounted(() => {
<template>
<div ref="div" class="hidden"></div>
<div v-if="docsAddon.visible" :class="[
'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',
{
'border-primary/50': !docsAddon.active && docsAddon.visible,
'border-primary scale-105': docsAddon.active,
},
]" @dragenter="docsAddon.active = true" @dragleave="docsAddon.active = false" @dragover="(e) => e.preventDefault()"
@drop="onDrop">
<div
v-if="docsAddon.visible"
:class="[
'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',
{
'border-primary/50': !docsAddon.active && docsAddon.visible,
'border-primary scale-105': docsAddon.active,
},
]"
@dragenter="docsAddon.active = true"
@dragleave="docsAddon.active = false"
@dragover="(e) => e.preventDefault()"
@drop="onDrop"
>
<div class="pointer-events-none">
<div class="flex items-center gap-2">
<IconNoteStackAdd class="w-8 h-8" />
......@@ -168,23 +175,39 @@ onUnmounted(() => {
</div>
</div>
<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',
'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,
},
]">
<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
v-if="chatDocsPanel.visible"
ref="chatDocsDiv"
:class="[
'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',
{
'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"
>
<img :src="logoUrl" class="w-6 h-6" />
<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"
@click="chatDocsPanel.visible = false">
<button
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" />
</button>
</div>
<ChatDocsPanel @close="chatDocsPanel.visible = false" :siteConfig="siteConfig" />
<ChatDocsPanel
@close="chatDocsPanel.visible = false"
:siteConfig="siteConfig"
/>
</div>
</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() {
</script>
<template>
<div v-if="pipWindow.tab && pipWindow.tab.id">
<div class="text-sm flex items-center truncate mt-6">
<span
class="w-4 h-4 inline-block mr-2 rounded"
:style="{
background:
'#8882 center / contain url(' + pipWindow.tab?.favIconUrl + ')',
}"
></span>
<span>{{ pipWindow.tab?.title }}</span>
</div>
<div class="flex gap-2">
<button
class="primary-btn flex items-center mt-2 rounded-lg p-2 px-3"
@click="handleUpdatePip('minimized')"
>
<IconHide />
</button>
<button
class="primary-btn flex items-center mt-2 rounded-lg p-2 px-3"
@click="handleUpdatePip('normal')"
>
<IconArrowCircleRight />
</button>
<button
class="primary-btn flex items-center mt-2 rounded-lg p-2 px-3"
@click="closePip"
>
<IconClose />
</button>
</div>
<div class="justify-between border-2 border-solid border-background-mute">
<div
class="size-7 rounded mr-auto"
:style="{
background: `#8881 center / contain url('${pipWindow.tab?.favIconUrl}')`,
}"
></div>
<button
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')"
>
<IconHide class="size-5" />
</button>
<button
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')"
>
<IconArrowCircleRight class="size-5" />
</button>
<button
class="bg-background-soft hover:bg-background-mute rounded-full size-8 p-1 flex items-center justify-center"
@click="closePip"
>
<IconClose class="size-5" />
</button>
</div>
</template>
<style scoped>
.primary-btn {
background: var(--color-background-soft);
}
.primary-btn:hover {
background: var(--color-background-mute);
}
</style>
<style scoped></style>
......@@ -15,6 +15,4 @@ const { t } = useI18n()
/>
<ChatDocsAddon />
</template>
<style scoped></style>
</template>
\ No newline at end of file
import type { ContentEventType } from "@/types"
type EventOptions =
| {
type: "pip"
type: ContentEventType.pip
detail: {
url: string
mode: string
}
}
| {
type: "load-doc"
type: ContentEventType.pipLoad
detail: {
url: string
}
}
| {
type: "loaded"
type: ContentEventType.pipLoaded
detail: {}
}
type EventType = EventOptions["type"]
function getRealType(type: string) {
return "anything-copilot_" + type
}
| {
type: ContentEventType.escapeLoad
detail: {
url: string
}
}
export function dispatchContentEvent({ type, detail }: EventOptions) {
const event = new CustomEvent(getRealType(type), { detail })
const event = new CustomEvent(type, { detail })
document.dispatchEvent(event)
}
export function addContentEventListener(
type: EventType,
type: ContentEventType,
handler: (e: Event) => void
) {
document.addEventListener(getRealType(type), handler)
document.addEventListener(type, handler)
}
export function removeContentEventListener(
type: EventType,
type: ContentEventType,
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 {
chatDocsPanel,
pipLauncher,
pipLoading,
pipWindow,
} from "@/store"
import { MessageType } from "@/types"
import { chatDocsPanel, pipLauncher, pipLoading, pipWindow } from "@/store"
import { ContentEventType, FrameMessageType, MessageType } from "@/types"
import Copilot from "./Copilot.vue"
import { waitMessage } from "@/utils/ext"
import {
......@@ -14,6 +9,7 @@ import {
removeContentEventListener,
} from "@/content/event"
import { contentService } from "@/utils/service"
import { getPageIcon } from "@/utils/dom"
// import { PipEventName } from "@/types/pip"
function handleMessage(
......@@ -25,7 +21,7 @@ function handleMessage(
switch (message?.type) {
case MessageType.pip:
dispatchContentEvent({
type: "pip",
type: ContentEventType.pip,
detail: message.options,
})
break
......@@ -73,9 +69,9 @@ async function handlePipEvent(event: any) {
const handlePipLoaded = (e: Event) => {
console.log("load", e)
r()
removeContentEventListener("loaded", handlePipLoaded)
removeContentEventListener(ContentEventType.pipLoaded, handlePipLoaded)
}
addContentEventListener("loaded", handlePipLoaded)
addContentEventListener(ContentEventType.pipLoaded, handlePipLoaded)
})
// may be 0 if not wait document is loaded
......@@ -109,11 +105,52 @@ async function handlePopLoadDocEvent(e: CustomEvent | Event) {
pipLoading.isLoading = true
}
chrome.runtime?.onMessage.addListener(handleMessage)
addContentEventListener("pip", handlePipEvent)
addContentEventListener("loaded", handlePipLoadedEvent)
addContentEventListener("load-doc", handlePopLoadDocEvent)
waitMountApp()
function run() {
chrome.runtime?.onMessage.addListener(handleMessage)
addContentEventListener(ContentEventType.pip, handlePipEvent)
addContentEventListener(ContentEventType.pipLoaded, handlePipLoadedEvent)
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
if (location.host == chrome.runtime.id && location.hash == "#copilot") {
......
import { ContentEventType } from "@/types"
import { addContentEventListener } from "./event"
import { copilotNavigateTo, pip } from "./pip"
import { copilotNavigateTo, pip, fetchDoc, writeHtml } from "./pip"
function handlePipEvent(event: CustomEvent | Event) {
if ("detail" in event) {
......@@ -7,11 +8,22 @@ function handlePipEvent(event: CustomEvent | Event) {
}
}
function handleLoadDocEvent(event: CustomEvent | Event) {
function handlePipLoadDocEvent(event: CustomEvent | Event) {
if ("detail" in event) {
copilotNavigateTo(event.detail.url)
}
}
addContentEventListener('pip', handlePipEvent)
addContentEventListener('load-doc', handleLoadDocEvent)
async function handleEscapeLoadEvent(event: CustomEvent | Event) {
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 {
removePrerenderRules,
} from "@/utils/dom"
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 = {
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",
......@@ -85,8 +86,8 @@ export async function copilotNavigateTo(url: string) {
const res = await fetchDoc(url)
const html = await res.text()
// resetWindow(pipWindow);
writeHtml(pipWindow, html)
pipWindow.history.replaceState(pipWindow.history.state, "", url)
writeHtml(pipWindow, html)
}
type ReopenOptions = {
......@@ -115,7 +116,7 @@ export async function copilotReopen({ url, width, height }: ReopenOptions) {
navGuard(pipWindow)
}
function writeHtml(pipWindow: Window, html: string) {
export function writeHtml(pipWindow: Window, html: string) {
const nonce = getDomNonce(document)
let escaped = replaceHtmlNonce(html, nonce)
escaped = getTrustedHTML(escaped)
......@@ -123,7 +124,7 @@ function writeHtml(pipWindow: Window, html: string) {
pipWindow.document.open()
pipWindow.document.write(escaped)
pipWindow.document.close()
dispatchContentEvent({ type: "loaded", detail: {} })
dispatchContentEvent({ type: ContentEventType.pipLoaded, detail: {} })
const base = document.createElement("base")
base.target = "_blank"
......
......@@ -2,7 +2,8 @@ import { createApp, type Component } from "vue"
import App from "./App.vue"
import { MessageType } from "@/types"
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
......@@ -11,11 +12,17 @@ export function mount(App: Component, doc = document) {
const root = isSelf ? outter : outter.attachShadow({ mode: "open" })
const appContainer = doc.createElement("div")
appContainer.id = "app"
// appContainer.setAttribute("part", "app")
const link = doc.createElement("link")
link.rel = "stylesheet"
link.href = chrome.runtime?.getURL("/assets/index.css")
const style = doc.createElement("style")
style.innerHTML = styles
root.append(link)
root.append(style)
root.append(appContainer)
doc.documentElement.append(outter)
......@@ -30,7 +37,7 @@ export function mountApp(doc = document) {
}
export function waitMountApp() {
if (document.readyState == "interactive") {
if (["interactive", "complete"].includes(document.readyState)) {
mountApp()
} else {
const hanldeStateChange = () => {
......
{
"openInPip": "በCopilot የምጥ ገጽ ክፍል ክፈት",
"other": "ሌሎች",
"openInPip": "ብቅ ባለበት ክፍት ነው",
"openInSidebar": "በጎን አሞሌ ውስጥ ይክፈቱ",
"popular": "ታዋቂ",
"sidebar": "የጎን አሞሌ",
"clickHere": "እዚህ ጠቅ ያድርጉ",
"minimize": "ምንጭ",
"moveAside": "ቀስት ለማግኘት",
......
{
"openInPip": "افتح في نافذة كوبيلوت",
"other": "آخر",
"openInPip": "مفتوح في المنبثقة",
"openInSidebar": "مفتوح في الشريط الجانبي",
"popular": "شائع",
"sidebar": "الشريط الجانبي",
"clickHere": "انقر هنا",
"minimize": "تصغير",
"moveAside": "تحريك جانباً",
......
{
"openInPip": "Отвори в Copilot прозорец",
"other": "Друго",
"openInPip": "Отворен в изскачащ прозорец",
"openInSidebar": "Отворен в страничната лента",
"popular": "Популярен",
"sidebar": "Странична лента",
"clickHere": "Цъкни тук",
"minimize": "Минимизиране",
"moveAside": "Премести настрана",
......
{
"openInPip": "Copilot উইন্ডোতে খুলুন",
"other": "অন্যান্য",
"openInPip": "পপআপে খুলুন",
"openInSidebar": "সাইডবারে খোলা",
"popular": "জনপ্রিয়",
"sidebar": "সাইডবার",
"clickHere": "এখানে ক্লিক করুন",
"minimize": "সর্বনিম্ন",
"moveAside": "পাশে সরান",
......
{
"openInPip": "Obri en finestra del Copilot",
"other": "Altres",
"openInPip": "Obert a la finestra emergent",
"openInSidebar": "Obert a la barra lateral",
"popular": "Popular",
"sidebar": "Barra lateral",
"clickHere": "Clica aquí",
"minimize": "Minimitza",
"moveAside": "Aparta",
......
{
"openInPip": "Otevřít v okně Copilot",
"other": "Další",
"openInPip": "Otevřeno v vyskakovacím okně",
"openInSidebar": "Otevřeno v postranním panelu",
"popular": "Oblíbený",
"sidebar": "Postranní panel",
"clickHere": "Klikněte zde",
"minimize": "Minimalizovat",
"moveAside": "Přesunout stranou",
......
{
"openInPip": "Åbn i Copilot-vindue",
"other": "Andet",
"openInPip": "Åben i popup",
"openInSidebar": "Åben i sidebjælken",
"popular": "Populær",
"sidebar": "Sidebjælke",
"clickHere": "Klik her",
"minimize": "Minimer",
"moveAside": "Flyt til side",
......
{
"openInPip": "In Copilot-Fenster öffnen",
"other": "Andere",
"openInPip": "Offen im Popup",
"openInSidebar": "In der Seitenleiste geöffnet",
"popular": "Beliebt",
"sidebar": "Seitenleiste",
"clickHere": "Hier klicken",
"minimize": "Minimieren",
"moveAside": "Beiseite bewegen",
......
{
"openInPip": "Ανοίξτε στο παράθυρο του Copilot",
"other": "Άλλο",
"openInPip": "Ανοίξτε στο αναδυόμενο παράθυρο",
"openInSidebar": "Ανοίξτε στην πλαϊνή μπάρα",
"popular": "Δημοφιλής",
"sidebar": "Πλευρική γραμμή",
"clickHere": "Κάντε κλικ εδώ",
"minimize": "Ελαχιστοποίηση",
"moveAside": "Μετακίνηση στην πλευρά",
......
{
"openInPip": "Open in Copilot window",
"other": "Other",
"openInPip": "Open in Popup",
"openInSidebar": "Open in Sidebar",
"popular": "Popular",
"sidebar": "Sidebar",
"clickHere": "Click Here",
"minimize": "Minimize",
"moveAside": "Move aside",
......
{
"openInPip": "Abrir en ventana de Copilot",
"other": "Otro",
"openInPip": "Abrir en ventana emergente",
"openInSidebar": "Abrir en la barra lateral",
"popular": "Popular",
"sidebar": "Barra lateral",
"clickHere": "Haz clic aquí",
"minimize": "Minimizar",
"moveAside": "Mover a un lado",
......
{
"openInPip": "Abrir en ventana de Copilot",
"other": "Otro",
"openInPip": "Abrir en ventana emergente",
"openInSidebar": "Abrir en la barra lateral",
"popular": "Popular",
"sidebar": "Barra lateral",
"clickHere": "Haz clic aquí",
"minimize": "Minimizar",
"moveAside": "Mover a un lado",
......
{
"openInPip": "Avage Copiloti aken",
"other": "Muu",
"openInPip": "Avatud hüpikus",
"openInSidebar": "Avatud külgribal",
"popular": "Populaarne",
"sidebar": "Külgriba",
"clickHere": "Klõpsake siin",
"minimize": "Vähenda",
"moveAside": "Liiguta kõrvale",
......
{
"openInPip": "باز کردن در پنجره Copilot",
"other": "سایر",
"openInPip": "باز در پنجره",
"openInSidebar": "در نوار کناری باز می شود",
"popular": "محبوب",
"sidebar": "نوار کناری",
"clickHere": "اینجا کلیک کنید",
"minimize": "کمینه کردن",
"moveAside": "جابجا شدن",
......
{
"openInPip": "Avaa Copilot-ikkunassa",
"other": "Muu",
"openInPip": "Avoin ponnahdusikkuna",
"openInSidebar": "Avoinna sivupalkissa",
"popular": "Suosittu",
"sidebar": "Sivupalkki",
"clickHere": "Klikkaa tästä",
"minimize": "Pienennä",
"moveAside": "Siirrä sivuun",
......
{
"openInPip": "Buksan sa bintana ng Copilot",
"other": "Iba pa",
"openInPip": "Buksan sa Popup",
"openInSidebar": "Buksan sa sidebar",
"popular": "Tanyag",
"sidebar": "Sidebar",
"clickHere": "I-click Dito",
"minimize": "Ibawas",
"moveAside": "Ilipat sa tabi",
......
{
"openInPip": "Ouvrir dans la fenêtre Copilot",
"other": "Autre",
"openInPip": "Ouvert en popup",
"openInSidebar": "Ouvert dans la barre latérale",
"popular": "Populaire",
"sidebar": "Barre latérale",
"clickHere": "Cliquez ici",
"minimize": "Minimiser",
"moveAside": "Déplacer de côté",
......
{
"openInPip": "કોપિલોટ વિંડોમાં ખોલો",
"other": "અન્ય",
"openInPip": "પ pop પઅપ ખોલો",
"openInSidebar": "સાઇડબારમાં ખોલો",
"popular": "પ્રખ્યાત",
"sidebar": "લારીક",
"clickHere": "અહીં ક્લિક કરો",
"minimize": "ઘટાડો",
"moveAside": "અપર ચાલો",
......
{
"openInPip": "פתח בחלון Copilot",
"other": "אחר",
"openInPip": "פתוח בקופץ",
"openInSidebar": "פתוח בסרגל הצד",
"popular": "פופולרי",
"sidebar": "סרגל צד",
"clickHere": "לחץ כאן",
"minimize": "מזער",
"moveAside": "הזז בצד",
......
{
"openInPip": "कोपाइलट विंडो में खोलें",
"other": "अन्य",
"openInPip": "पॉपअप में खुला",
"openInSidebar": "साइडबार में खुला",
"popular": "लोकप्रिय",
"sidebar": "साइड बार",
"clickHere": "यहां क्लिक करें",
"minimize": "कम करें",
"moveAside": "दूर हटें",
......
{
"openInPip": "Otvori u CoPilot prozoru",
"other": "Ostalo",
"openInPip": "Otvoreno u skočnom prozoru",
"openInSidebar": "Otvoreno na bočnoj traci",
"popular": "Popularan",
"sidebar": "Bočna traka",
"clickHere": "Kliknite ovdje",
"minimize": "Smanji",
"moveAside": "Pomakni na stranu",
......
{
"openInPip": "Megnyitás a Copilot ablakban",
"other": "Egyéb",
"openInPip": "Nyissa meg a felbukkanó felbukkanást",
"openInSidebar": "Nyitva az oldalsávban",
"popular": "Népszerű",
"sidebar": "Oldalsáv",
"clickHere": "Kattintson ide",
"minimize": "Kis méret",
"moveAside": "Elmozdít",
......
{
"openInPip": "Buka dalam jendela Copilot",
"other": "Lainnya",
"openInPip": "Buka di popup",
"openInSidebar": "Buka di bilah sisi",
"popular": "Populer",
"sidebar": "Bilah sisi",
"clickHere": "Klik Disini",
"minimize": "Miminimalkan",
"moveAside": "Pindah ke samping",
......
{
"openInPip": "Apri nella finestra di Copilot",
"other": "Altro",
"openInPip": "Aperto nel popup",
"openInSidebar": "Aperto nella barra laterale",
"popular": "Popolare",
"sidebar": "Barra laterale",
"clickHere": "Clicca qui",
"minimize": "Minimizza",
"moveAside": "Sposta da parte",
......
{
"openInPip": "コパイロットウィンドウで開く",
"other": "その他",
"openInPip": "ポップアップで開いています",
"openInSidebar": "サイドバーで開きます",
"popular": "人気のある",
"sidebar": "サイドバー",
"clickHere": "ここをクリック",
"minimize": "最小化",
"moveAside": "一旦寄せる",
......
{
"openInPip": "ಕೊಪಿಲೋಟ್ ವಿಂಡೋದಲ್ಲಿ ತೆರೆಯಿರಿ",
"other": "ಇತರೆ",
"openInPip": "ಪಾಪ್ಅಪ್ನಲ್ಲಿ ತೆರೆಯಿರಿ",
"openInSidebar": "ಸೈಡ್‌ಬಾರ್‌ನಲ್ಲಿ ತೆರೆಯಿರಿ",
"popular": "ಜನಪ್ರಿಯ",
"sidebar": "ಪಕ್ಕದ ಬಾರ್ನ",
"clickHere": "ಇಲ್ಲಿ ಕ್ಲಿಕ್ ಮಾಡಿ",
"minimize": "ಕುಗ್ಗಿಸಿ",
"moveAside": "ಬೆಳಕು ನೀಡಿ",
......
{
"openInPip": "Copilot 창에서 열기",
"other": "기타",
"openInPip": "팝업에서 열립니다",
"openInSidebar": "사이드 바에서 열립니다",
"popular": "인기 있는",
"sidebar": "사이드 바",
"clickHere": "여기를 클릭하세요",
"minimize": "최소화",
"moveAside": "옆으로 이동",
......
{
"openInPip": "Atidaryti „Copilot“ langą",
"other": "Kitas",
"openInPip": "Atidaryti iššokantįjį",
"openInSidebar": "Atidarykite šoninėje juostoje",
"popular": "Populiarus",
"sidebar": "Šoninė juosta",
"clickHere": "Spustelėkite čia",
"minimize": "Mažinti",
"moveAside": "Nustumti šalin",
......
{
"openInPip": "Atvērt Copilot logā",
"other": "Cits",
"openInPip": "Atvērts uznirstošajā logā",
"openInSidebar": "Atveriet sānjoslā",
"popular": "Populārs",
"sidebar": "Sānjosla",
"clickHere": "Noklikšķiniet šeit",
"minimize": "Minimizēt",
"moveAside": "Pagriezt malā",
......
{
"openInPip": "കോപിലോറ്റ് ജിപ്പി ജോളം തുറക്കുക",
"other": "മറ്റ്",
"openInPip": "പോപ്പ്അപ്പിൽ തുറക്കുക",
"openInSidebar": "സൈഡ്ബാറിൽ തുറക്കുക",
"popular": "ജനപീതിയായ",
"sidebar": "സൈഡ്ബാർ",
"clickHere": "ഇവിടെ ക്ലിക്ക് ചെയ്യുക",
"minimize": "ഇടത്തരിക്കുക",
"moveAside": "വലത്തരിക്കുക",
......
{
"openInPip": "कोपायलटच्या विंडोमध्ये उघडा",
"other": "इतर",
"openInPip": "पॉपअप मध्ये उघडा",
"openInSidebar": "साइडबार मध्ये उघडा",
"popular": "लोकप्रिय",
"sidebar": "साइडबार",
"clickHere": "येथे क्लिक करा",
"minimize": "कमी करा",
"moveAside": "पाठवा",
......
{
"openInPip": "Buka dalam tetingkap Copilot",
"other": "Lain-lain",
"openInPip": "Buka dalam popup",
"openInSidebar": "Buka di bar sisi",
"popular": "Popular",
"sidebar": "Sidebar",
"clickHere": "Klik di sini",
"minimize": "Kurangkan",
"moveAside": "Bersisih",
......
{
"openInPip": "Open in Copilot-venster",
"other": "Andere",
"openInPip": "Open in pop -up",
"openInSidebar": "Open in zijbalk",
"popular": "Populair",
"sidebar": "Zijbalk",
"clickHere": "Klik hier",
"minimize": "Minimaliseren",
"moveAside": "Opzij zetten",
......
{
"openInPip": "Åpne i Copilot-vindu",
"other": "Annet",
"openInPip": "Åpent i popup",
"openInSidebar": "Åpent i sidefeltet",
"popular": "Populær",
"sidebar": "Sidefelt",
"clickHere": "Klikk her",
"minimize": "Minimer",
"moveAside": "Flytt til side",
......
{
"openInPip": "Otwórz w oknie Copilot",
"other": "Inne",
"openInPip": "Otwarte w wyskakującym okienku",
"openInSidebar": "Otwarte na pasku bocznym",
"popular": "Popularny",
"sidebar": "Pasek boczny",
"clickHere": "Kliknij tutaj",
"minimize": "Zminimalizuj",
"moveAside": "Przesuń na bok",
......
{
"openInPip": "Abrir na janela do Copilot",
"other": "Outro",
"openInPip": "Aberto em pop -up",
"openInSidebar": "Aberto na barra lateral",
"popular": "Popular",
"sidebar": "Barra Lateral",
"clickHere": "Clique aqui",
"minimize": "Minimizar",
"moveAside": "Mover para o lado",
......
{
"openInPip": "Abrir em janela do Copilot",
"other": "Outro",
"openInPip": "Aberto em pop -up",
"openInSidebar": "Aberto na barra lateral",
"popular": "Popular",
"sidebar": "Barra Lateral",
"clickHere": "Clique aqui",
"minimize": "Minimizar",
"moveAside": "Mover para o lado",
......
{
"openInPip": "Deschide în fereastra Copilot",
"other": "Altele",
"openInPip": "Deschis în pop -up",
"openInSidebar": "Deschis în bara laterală",
"popular": "Popular",
"sidebar": "Bara laterală",
"clickHere": "Click aici",
"minimize": "Minimizează",
"moveAside": "Mută în lateral",
......
{
"openInPip": "Открыть в окне Copilot",
"other": "Другое",
"openInPip": "Открыт во всплывающем окне",
"openInSidebar": "Открыт на боковой панели",
"popular": "Популярный",
"sidebar": "Боковая панель",
"clickHere": "Нажмите здесь",
"minimize": "Минимизировать",
"moveAside": "Убрать в сторону",
......
{
"openInPip": "Otvoriť v okne Copilot",
"other": "Iné",
"openInPip": "Otvorené v kontextovom konte",
"openInSidebar": "Otvorené na bočnom paneli",
"popular": "Populárny",
"sidebar": "Bočný panel",
"clickHere": "Kliknite sem",
"minimize": "Minimalizovať",
"moveAside": "Presunúť bokom",
......
{
"openInPip": "Odpri v oknu Copilot",
"other": "Drugo",
"openInPip": "Odprt v pojavnem oknu",
"openInSidebar": "Odprto v stranski vrstici",
"popular": "Priljubljen",
"sidebar": "Stranska vrstica",
"clickHere": "Klikni tukaj",
"minimize": "Minimaliziraj",
"moveAside": "Premakni na stran",
......
{
"openInPip": "Отвори у прозору Копилота",
"other": "Остало",
"openInPip": "Отворен у скочном прозору",
"openInSidebar": "Отвори у бочној траци",
"popular": "Популаран",
"sidebar": "Бочна трака",
"clickHere": "Кликни овде",
"minimize": "Minimiziraj",
"moveAside": "Pomeri na stranu",
......
{
"openInPip": "Öppna i Copilot-fönster",
"other": "Annan",
"openInPip": "Öppen i popup",
"openInSidebar": "Öppet i sidofältet",
"popular": "Populär",
"sidebar": "Sidofält",
"clickHere": "Klicka här",
"minimize": "Minimera",
"moveAside": "Flytta åt sidan",
......
{
"openInPip": "Fungua kwenye dirisha la Copilot",
"other": "Nyingine",
"openInPip": "Fungua katika kidukizo",
"openInSidebar": "Fungua katika pembeni",
"popular": "Maarufu",
"sidebar": "Pembeni",
"clickHere": "Bonyeza Hapa",
"minimize": "Kupunguza",
"moveAside": "Hama kando",
......
{
"openInPip": "கிளிக் மேல் இங்கே திற",
"other": "பிற",
"openInPip": "பாப்அப்பில் திறந்திருக்கும்",
"openInSidebar": "பக்கப்பட்டியில் திறக்கவும்",
"popular": "மக்கள்",
"sidebar": "பக்கப்பட்டி",
"clickHere": "இங்கே திட்டமிடு",
"minimize": "சுருக்கப்படுதல்",
"moveAside": "பக்கத்திற்கு நீக்கு",
......
{
"openInPip": "కొపీలోట్ విండోలో తెరలంచండి",
"other": "ఇతర",
"openInPip": "పాపప్‌లో తెరవండి",
"openInSidebar": "సైడ్‌బార్‌లో తెరవండి",
"popular": "జనాదరణ పొందింది",
"sidebar": "సైడ్‌బార్",
"clickHere": "ఇక్కడ రాండండి",
"minimize": "తక్కువ చేయడం",
"moveAside": "చేరుకుందండి",
......
{
"openInPip": "เปิดในหน้าต่าง Copilot",
"other": "อื่น ๆ",
"openInPip": "เปิดในป๊อปอัป",
"openInSidebar": "เปิดในแถบด้านข้าง",
"popular": "เป็นที่นิยม",
"sidebar": "แถบด้านข้าง",
"clickHere": "คลิกที่นี่",
"minimize": "ย่อ",
"moveAside": "เลื่อนข้างหลัง",
......
{
"openInPip": "Copilot penceresinde aç",
"other": "Diğer",
"openInPip": "Açılır pencerede açık",
"openInSidebar": "Kenar çubuğunda açık",
"popular": "Popüler",
"sidebar": "Kenar çubuğu",
"clickHere": "Buraya tıklayın",
"minimize": "Küçült",
"moveAside": "Kenara Taşı",
......
{
"openInPip": "Відкрити у вікні Copilot",
"other": "Інше",
"openInPip": "Відкрито у спливаючому віці",
"openInSidebar": "Відкрито на бічній панелі",
"popular": "Популярний",
"sidebar": "Бічна панель",
"clickHere": "Натисніть тут",
"minimize": "Мінімізувати",
"moveAside": "Відкрити сторонній",
......
{
"openInPip": "Mở trong cửa sổ Copilot",
"other": "Khác",
"openInPip": "Mở cửa bật lên",
"openInSidebar": "Mở trong thanh bên",
"popular": "Phổ biến",
"sidebar": "Thanh bên",
"clickHere": "Nhấp vào đây",
"minimize": "Thu nhỏ",
"moveAside": "Di chuyển sang một bên",
......
{
"openInPip": "在 Copilot 窗口中打开",
"other": "其他",
"openInPip": "在弹出窗口中打开",
"openInSidebar": "在侧边栏中打开",
"popular": "受欢迎的",
"sidebar": "侧边栏",
"clickHere": "点击这里",
"minimize": "最小化",
"moveAside": "移到旁边",
......
{
"openInPip": "在 Copilot 窗口中打開",
"other": "其他",
"openInPip": "在彈出窗口中打開",
"openInSidebar": "在側邊欄中打開",
"popular": "受歡迎的",
"sidebar": "側邊欄",
"clickHere": "點擊這裡",
"minimize": "最小化",
"moveAside": "移到旁邊",
......
import { ContentScriptId } from "./types"
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 = {
manifest_version: 3,
......@@ -10,7 +29,7 @@ const manifest = {
// short_name: "__MSG_short_name__",
// no more than 132 characters
description: "__MSG_description__",
version: "1.2.1",
version: "1.2.3",
action: {
default_icon: {
16: "logo.png",
......@@ -18,7 +37,7 @@ const manifest = {
32: "logo.png",
},
default_title: "__MSG_short_name__",
default_popup: "src/pages/popup.html",
default_popup: "popup.html",
},
default_locale: "en",
icons: {
......@@ -27,26 +46,40 @@ const manifest = {
48: "logo.png",
128: "logo.png",
},
author: "support@ziziyi.com",
author: { email: "support@ziziyi.com" },
background: {
service_worker: "bg.js",
type: "module",
service_worker: "./src/bg/index.ts",
type: "module" as const,
},
content_scripts: [
// {
// matches: ["<all_urls>"],
// js: ["src/content/main.ts"],
// run_at: "document_start",
// world: "MAIN",
// },
{
matches: ["<all_urls>"],
js: ["/js/content-main.js"],
run_at: "document_start",
world: "MAIN",
},
{
matches: ["<all_urls>"],
js: ["/js/content.js"],
matches: ["http://placeholder.ziziyi.com/*"],
js: ["src/content/index.ts"],
run_at: "document_start",
},
],
options_page: __DEV__ ? "/src/pages/guide.html" : "",
permissions: ["tabs", "scripting", "activeTab", "storage", "offscreen"],
options_page: __DEV__ ? "sidebar.html" : undefined,
side_panel: {
default_path: defaultSidebarPath,
},
permissions: [
"tabs",
"scripting",
"activeTab",
"storage",
"offscreen",
"sidePanel",
"declarativeNetRequestWithHostAccess",
"declarativeNetRequestFeedback",
"webNavigation",
],
optional_permissions: [],
host_permissions: ["<all_urls>"],
minimum_chrome_version: "111",
commands: {
......@@ -60,8 +93,13 @@ const manifest = {
},
web_accessible_resources: [
{
resources: [contentCss, "logo.svg"],
resources: ["logo.svg"],
matches: ["<all_urls>"],
},
{
resources: ["/js/*", "/assets/*"],
matches: ["<all_urls>"],
use_dynamic_url: true,
},
],
content_security_policy: {
......@@ -69,6 +107,6 @@ const manifest = {
? `script-src 'self' http://localhost:3000 'wasm-unsafe-eval';`
: `script-src 'self' 'wasm-unsafe-eval'`,
},
}
} satisfies Manifest as chrome.runtime.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 "@/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"
/** 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({
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 {
invokeRequest = "invoke-request",
invokeResponse = "invoke-Response",
showChatDocs = "show-chat-docs",
frameReady = "frame-ready",
}
export enum ServiceFunc {
......@@ -31,3 +32,23 @@ export type ParseDocOptions = {
size: number
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(
}
}
}
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 = {
tabId: number;
status: string;
timeout?: number;
};
tabId: number
status: string
timeout?: number
}
export async function tabUpdated({ tabId, status, timeout }: UpdatedOption) {
return new Promise<void>((r) => {
const handleUpdate = (id: number, info: chrome.tabs.TabChangeInfo) => {
console.log(id, info);
console.log(id, info)
if (id === tabId && info.status === status) {
chrome.tabs.onUpdated.removeListener(handleUpdate);
r();
chrome.tabs.onUpdated.removeListener(handleUpdate)
r()
}
};
}
setTimeout(() => {
chrome.tabs.onUpdated.removeListener(handleUpdate);
r();
}, timeout || 30 * 1000);
chrome.tabs.onUpdated.addListener(handleUpdate);
});
chrome.tabs.onUpdated.removeListener(handleUpdate)
r()
}, timeout || 30 * 1000)
chrome.tabs.onUpdated.addListener(handleUpdate)
})
}
type WaitMessageOption = {
type: string;
tabId?: number;
timeout?: number;
};
type: string
tabId?: number
timeout?: number
}
export async function waitMessage<T = unknown>({
type,
......@@ -37,23 +38,23 @@ export async function waitMessage<T = unknown>({
timeout,
}: WaitMessageOption): Promise<T> {
return new Promise<T>((r, reject) => {
let timer = 0;
let timer = 0
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) {
chrome.runtime.onMessage.removeListener(handleMessage);
r(message);
clearTimeout(timer);
chrome.runtime.onMessage.removeListener(handleMessage)
r(message)
clearTimeout(timer)
}
};
chrome.runtime.onMessage.addListener(handleMessage);
}
chrome.runtime.onMessage.addListener(handleMessage)
if (timeout) {
timer = window.setTimeout(() => {
chrome.runtime.onMessage.removeListener(handleMessage);
reject();
}, timeout);
chrome.runtime.onMessage.removeListener(handleMessage)
reject()
}, timeout)
}
});
})
}
export const emptyTab: chrome.tabs.Tab = {
......@@ -69,70 +70,71 @@ export const emptyTab: chrome.tabs.Tab = {
index: 0,
pinned: false,
windowId: 0,
};
}
export async function checkContent(tabId: number) {
let alive = false;
let alive = false
try {
const resMsgPromise = new Promise<void>((r) => {
const handleMessage = (message: any, sender: MessageSender) => {
if (sender.tab?.id == tabId) {
alive = true;
r();
chrome.runtime.onMessage.removeListener(handleMessage);
alive = true
r()
chrome.runtime.onMessage.removeListener(handleMessage)
}
};
setTimeout(() => r(), 3000);
chrome.runtime.onMessage.addListener(handleMessage);
});
}
setTimeout(() => r(), 3000)
chrome.runtime.onMessage.addListener(handleMessage)
})
const res = await chrome.tabs.sendMessage(tabId, {
type: MessageType.hiContent,
});
alive = !!res;
})
alive = !!res
console.log("hi-content response: ", alive, res);
console.log("hi-content response: ", alive, res)
if (!alive) {
await resMsgPromise;
await resMsgPromise
}
} catch (err) {
console.warn(err);
console.warn(err)
console.log("content is not available")
}
console.log("checkContent alive: ", alive);
console.log("checkContent alive: ", alive)
if (alive) {
return true;
return true
}
const manifest = chrome.runtime.getManifest();
const manifest = chrome.runtime.getManifest()
if (!manifest.content_scripts) {
return false;
return false
}
const contentScripts = [...manifest.content_scripts, mainContentScript]
try {
for (let item of manifest.content_scripts) {
for (let item of contentScripts) {
if (item.js) {
const world =
"world" in item && item.world == "MAIN" ? "MAIN" : "ISOLATED";
"world" in item && item.world == "MAIN" ? "MAIN" : "ISOLATED"
await chrome.scripting.executeScript({
files: item.js,
target: { tabId: tabId },
world: world,
});
})
}
}
return true;
return true
} catch (e) {
return false;
return false
}
}
type StoreUrlOptions = {
id: string
name: string
......@@ -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>
}
export function getSession<T extends Record<string, any>>(key: string | 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
export function getLocale() {
if (__DEV__) {
return "en"
return "zh-CN"
}
const language = chrome.i18n.getUILanguage()
......
......@@ -50,3 +50,39 @@ export const semanticClip = (text: string, maxLength: number) => {
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} */
module.exports = {
export default {
content: ["./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
borderRadius: {
......@@ -14,22 +14,10 @@ module.exports = {
full: "9999px",
},
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" }],
sm: ["14px", { lineHeight: "20px" }],
base: ["16px", { lineHeight: "24px" }],
lg: ["18.px", { lineHeight: "28px" }],
lg: ["18px", { lineHeight: "28px" }],
xl: ["20px", { lineHeight: "28px" }],
"2xl": ["24px", { lineHeight: "32px" }],
"3xl": ["30px", { lineHeight: "36px" }],
......
......@@ -7,7 +7,7 @@
"src/**/*.json",
"tests/**/*"
],
"exclude": ["src/**/__tests__/*", "src/manifest.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"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
......@@ -20,7 +20,7 @@ export default function makeManifest(
if (chunk) {
manifest[key] = chunk.fileName
}
}
}
}
const content = JSON.stringify(manifest, null, 2)
......
......@@ -7,11 +7,15 @@ import vueJsx from "@vitejs/plugin-vue-jsx"
import vueI18n from "@intlify/unplugin-vue-i18n/vite"
import wasm from "vite-plugin-wasm"
import topLevelAwait from "vite-plugin-top-level-await"
import { crx } from "@crxjs/vite-plugin"
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" />
const __DEV__ = process.env.NODE_ENV == "development"
// https://vitejs.dev/config/
export default defineConfig({
define: {
......@@ -21,7 +25,11 @@ export default defineConfig({
},
plugins: [
vue(),
copy({
hook: "buildEnd",
targets: __DEV__ ? [{ src: "public/*", dest: "dist" }] : [],
}),
vue({}),
vueJsx(),
vueI18n({
runtimeOnly: true,
......@@ -30,32 +38,32 @@ export default defineConfig({
"./src/locales/**"
),
}),
makeManifest(manifest, { isDev: false }),
crx({ manifest, contentScripts: { injectCss: false } }),
wasm(),
topLevelAwait(),
],
resolve: {
alias: {
"@": 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: {
target: ["chrome111"],
emptyOutDir: false,
assetsDir: "assets",
emptyOutDir: true,
// cssCodeSplit: false,
outDir: "dist",
rollupOptions: {
input: {
popup: "src/pages/popup.html",
guide: "src/pages/guide.html",
worker: "src/pages/offscreen.html",
// dev: "src/pages/dev.html",
offscreen: "offscreen.html",
},
output: {
assetFileNames: "assets/[name].[ext]",
chunkFileNames: "js/[name]-chunk.js",
entryFileNames: "js/[name].js",
chunkFileNames: "js/chunk-[hash].js",
assetFileNames: "assets/[name][extname]",
},
},
},
......
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