Commit df56514b authored by Domi's avatar Domi

feat: loading & shortcut

parent b3c31913
/// <reference types="vite/client" />
interface DocumentPictureInPicture {
interface DocumentPictureInPicture extends EventTarget {
window: Window | null;
requestWindow(option?: { width?: number; height?: number }): Promise<Window>;
}
export declare global {
interface documentPictureInPicture extends DocumentPictureInPicture {}
interface Window {
documentPictureInPicture: DocumentPictureInPicture;
trustedTypes: any
trustedTypes: any;
}
interface Navigator {
userAgentData: {
platform: string;
};
}
}
export {};
const contentCss = "index.css";
const manifest = {
manifest_version: 3,
name: "Anything Copilot",
description:
"Anything Copilot opens any page, including AI tools like ChatGPT, GPTs, Bard, and Claude, in pop-ups for multitasking.",
version: "1.0.0",
// maximum of 45 characters
name: "__MSG_name__",
// edge 12 characters
// short_name: "__MSG_short_name__",
// no more than 132 characters
description: "__MSG_description__",
version: "1.1.1",
action: {
default_icon: {
16: "logo.png",
24: "logo.png",
32: "logo.png",
},
default_title: "pip",
default_title: "__MSG_short_name__",
default_popup: "popup.html",
},
default_locale: "en",
......@@ -39,15 +44,30 @@ const manifest = {
},
],
options_page: "guide.html",
permissions: ["tabs", "scripting", "activeTab"],
permissions: ["tabs", "scripting", "activeTab", "storage"],
host_permissions: ["<all_urls>"],
minimum_chrome_version: "111",
commands: {
toggleMinimize: {
suggested_key: {
default: "Ctrl+Shift+1",
},
description: "__MSG_toggle_minimize_desc__",
global: true,
},
},
web_accessible_resources: [
{
resources: ["index.css"],
resources: [contentCss],
matches: ["<all_urls>"],
},
],
content_security_policy:
process.env.NODE_ENV == "development"
? {
extension_pages: `script-src 'self' http://localhost:3000;`,
}
: undefined,
};
export default manifest;
This diff is collapsed.
......@@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
"dev:page": "vite build --watch",
"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",
"dev": "run-p dev:page dev:content dev:js",
......@@ -17,22 +17,25 @@
"zip": ""
},
"dependencies": {
"vue": "^3.3.4"
"lodash-es": "^4.17.21",
"vue": "^3.3.4",
"vue-i18n": "^9.7.0"
},
"devDependencies": {
"@tsconfig/node18": "^18.2.2",
"@types/chrome": "^0.0.251",
"@types/lodash-es": "^4.17.11",
"@types/node": "^18.18.5",
"@vitejs/plugin-vue": "^4.4.0",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"@vue/tsconfig": "^0.4.0",
"autoprefixer": "^10.4.16",
"cross-env": "^7.0.3",
"npm-run-all2": "^6.1.1",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"typescript": "~5.2.0",
"vite": "^4.4.11",
"vite-plugin-svgr": "^4.1.0",
"vue-tsc": "^1.8.19"
}
}
{
"A": {
"message": "A"
"name": {
"message": "Anything Copilot - Any web page as copilot"
},
"short_name": {
"message": "Anything Copilot"
},
"description": {
"message": "Use the official ChatGPT website or any other webpage for free as your AI copilot, including GPTs, GPT-4, or any new features."
},
"toggle_minimize_desc": {
"message": "Toggle show/hide Copilot window"
}
}
{
"A": {
"message": "A"
"name": {
"message": "Anything Copilot - 无限应用、AI助手"
},
"short_name": {
"message": "Anything Copilot"
},
"description": {
"message": "免费将 ChatGPT 官方网页或其他任何页面变为您的 AI 助手,GPTs、GPT4 任何新功能都能立即体验。也能与开源大模型、文心一言对话,用 Google Translate 翻译甚至抖音刷短视频"
},
"toggle_minimize_desc": {
"message": "切换显示/隐藏Copilot窗口"
}
}
<!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>Vite App</title>
<base href="http://localhost:3000">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/content/index.ts"></script>
<script type="module" src="/@vite/client"></script>
</body>
</html>
public/logo.png

1.54 KB | W: | H:

public/logo.png

1.6 KB | W: | H:

public/logo.png
public/logo.png
public/logo.png
public/logo.png
  • 2-up
  • Swipe
  • Onion skin
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="83" y="28" width="200" height="50" rx="8" transform="rotate(90 83 28)" fill="#3BABFD"/>
<rect x="153" y="28" width="200" height="50" rx="8" transform="rotate(90 153 28)" fill="#0087E8"/>
<rect x="223" y="28" width="200" height="50" rx="8" transform="rotate(90 223 28)" fill="#005592"/>
<rect x="75" y="12" width="232" height="58" rx="8" transform="rotate(90 75 12)" fill="#3BABFD"/>
<rect x="157" y="12" width="232" height="59" rx="8" transform="rotate(90 157 12)" fill="#0087E8"/>
<rect x="238" y="12" width="232" height="58" rx="8" transform="rotate(90 238 12)" fill="#005592"/>
</svg>
......@@ -24,9 +24,9 @@ a,
place-items: center;
}
#app {
/* #app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
} */
}
import { tabUpdated } from "@/utils/ext";
import { MessageType } from "@/types";
import { waitMessage, tabUpdated } from "@/utils/ext";
async function openPipBackground(url: string) {
const tab = await chrome.tabs.create({
......@@ -31,26 +32,103 @@ async function getContentCss(id: number, url: string) {
async function pipLaunch(url: string) {
const tab = await chrome.tabs.create({ url });
await tabUpdated({ tabId: tab.id!, status: "complete" });
await waitMessage({
tabId: tab.id!,
type: MessageType.contentMount,
});
chrome.tabs.sendMessage(tab.id!, {
type: "pip-launch",
type: MessageType.pipLaunch,
url: url,
});
}
type QueryOptions = {
windowId?: number;
width?: number;
height?: number;
};
async function getPipWindow(
id: number,
{ windowId, width, height }: QueryOptions
) {
if (windowId) {
const win = await chrome.windows.get(windowId);
chrome.tabs.sendMessage(id, {
type: MessageType.pipWinInfo,
window: win,
});
return win;
}
const windows = await chrome.windows.getAll({});
const win = windows.find((w) => w.width === width && w.height === height);
chrome.tabs.sendMessage(id, {
type: MessageType.pipWinInfo,
window: win,
});
return win;
}
type MinimizeOptions = {
windowId: number;
};
async function minimizePip({ windowId }: MinimizeOptions) {
await chrome.windows.update(windowId, { state: "minimized" });
}
type UpdatePipWinOption = {
windowId: number;
windowInfo: Partial<chrome.windows.UpdateInfo>;
};
async function updatePipWin({ windowId, windowInfo }: UpdatePipWinOption) {
await chrome.windows.update(windowId, windowInfo);
}
function handleMessage(message: any, sender: chrome.runtime.MessageSender) {
console.log("bg message: ", message, sender);
switch (message?.type) {
case "bg-open-pip":
case MessageType.bgOpenPip:
openPipBackground(message.url);
break;
case "get-content-css":
getContentCss(sender.tab?.id || 0, message.url);
break;
case "bg-pip-launch":
case MessageType.bgPipLaunch:
pipLaunch(message.url);
break;
case MessageType.getPipWinInfo:
getPipWindow(sender.tab?.id!, message.options);
break;
case MessageType.minimizePipWin:
minimizePip(message.options);
console.log(message, sender);
break;
case MessageType.updatePipWin:
updatePipWin(message.options);
break;
}
}
chrome.runtime.onMessage.addListener(handleMessage);
async function handleToggleMinimize() {
const { pipWindowId } = await chrome.storage.local.get({ pipWindowId: null });
if (!pipWindowId) return;
const windowInfo = await chrome.windows.get(pipWindowId);
if (!windowInfo) return;
await chrome.windows.update(pipWindowId, {
state: windowInfo.state == "minimized" ? "normal" : "minimized",
});
}
function handleCommand(command: string) {
console.log("command: ", command);
switch (command) {
case "toggleMinimize":
handleToggleMinimize();
break;
}
}
chrome.commands.onCommand.addListener(handleCommand);
<template>
<div class="progress h-1 w-full bg-blue-500/30 overflow-hidden">
<div class="run h-full w-1/5 bg-blue-500"></div>
</div>
</template>
<style scoped>
.run {
animation: run 1.5s ease-in-out infinite;
}
@keyframes run {
from {
transform: translateX(-100%);
}
to {
transform: translate(500%);
}
}
</style>
<script setup lang="ts">
import { ref, computed, watch, onMounted } 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 { pipWindowInfo, pipWindowRef } from "@/content/store";
import { throttle } from "lodash-es";
import IconRefresh from "./icons/IconRefresh.vue";
import { PipEventName } from "@/types/pip";
import { onUnmounted } from "vue";
const open = ref(false);
const timer = ref(0);
const btns = ref<HTMLDivElement>();
const pipRight = computed(() => {
const winInfo = pipWindowInfo.value;
const { width } = screen;
if (!winInfo) return 0;
if (!winInfo.width || !winInfo.left) return 0;
return width - winInfo.left - winInfo.width;
});
const pipLeft = computed(() => {
const winInfo = pipWindowInfo.value;
return winInfo?.left || 0;
});
const updateWindowInfo = throttle(() => {
const id = pipWindowInfo.value?.id;
console.log("updateWindowInfo", id);
if (id) {
chrome.runtime.sendMessage({
type: MessageType.getPipWinInfo,
options: {
windowId: id,
},
});
}
}, 3000);
const handleClick = (e: MouseEvent) => {
const target = e.composedPath()[0] as Element;
if (btns.value?.contains(target)) {
return;
}
open.value = false;
};
watch(open, (value, oldValue, onCleanup) => {
value && updateWindowInfo();
window.addEventListener("click", handleClick);
onCleanup(() => {
window.removeEventListener("click", handleClick);
});
});
const menuPointerEnter = () => {
clearTimeout(timer.value);
timer.value = setTimeout(() => (open.value = true), 200);
};
const menuPointerLeave = () => {
clearTimeout(timer.value);
timer.value = setTimeout(() => (open.value = false), 200);
};
const btnPointerEnter = () => {
clearTimeout(timer.value);
};
const btnPointerLeave = () => {
timer.value = setTimeout(() => (open.value = false), 350);
};
const minimize = () => {
const windowId = pipWindowInfo.value?.id;
if (!windowId) {
console.log("windowId is not set", pipWindowInfo.value);
return;
}
chrome.runtime.sendMessage({
type: MessageType.updatePipWin,
options: {
windowId: windowId,
windowInfo: {
state: "minimized",
},
},
});
};
const slideOver = () => {
const windowId = pipWindowInfo.value?.id;
if (!windowId) {
console.log("windowId is not set", pipWindowInfo.value);
return;
}
console.log("slide over: ", pipRight, pipLeft);
const left = Math.min(Math.max(pipRight.value, 60), screen.width - 60);
chrome.runtime.sendMessage({
type: MessageType.updatePipWin,
options: {
windowId,
windowInfo: {
left,
},
},
});
};
const close = () => {
const win = window.documentPictureInPicture.window;
win?.close();
};
const refresh = () => {
const pipWindow = pipWindowRef.value;
if (pipWindow) {
document.dispatchEvent(
new CustomEvent(PipEventName.loadDoc, {
detail: { url: pipWindow.location.href },
})
);
}
// const win = window.documentPictureInPicture.window;
// win?.close();
// document.dispatchEvent(
// new CustomEvent(PipEventName.pip, {
// detail: {
// url: location.href,
// mode: "write-html",
// },
// })
// );
};
const throttledRefresh = throttle(refresh, 3000);
const handleKeydown = (e: KeyboardEvent) => {
const isWindows = navigator.userAgentData.platform == "Windows";
if (e.code == "KeyR" && (isWindows ? e.ctrlKey : e.metaKey)) {
throttledRefresh();
}
// if (e.code == "KeyM" && e.metaKey) {
// e.preventDefault();
// e.stopPropagation()
// }
};
onMounted(() => {
pipWindowRef.value?.addEventListener("keydown", handleKeydown);
});
onUnmounted(() => {
pipWindowRef.value?.removeEventListener("keydown", handleKeydown);
});
</script>
<template>
<button
class="multitasking-menu flex gap-1 px-2 py-1.5 z-[9999]"
@click="open = true"
@pointerenter="menuPointerEnter"
@pointerleave="menuPointerLeave"
>
<div class="dot w-1.5 h-1.5 rounded-full"></div>
<div class="dot w-1.5 h-1.5 rounded-full"></div>
<div class="dot w-1.5 h-1.5 rounded-full"></div>
</button>
<div
v-if="open"
ref="btns"
class="btn-group flex px-2 py-1 gap-2 fixed rounded-full left-6 z-[9999]"
@pointerenter="btnPointerEnter"
@pointerleave="btnPointerLeave"
@click="open = false"
>
<button
class="btn w-7 h-7 flex items-center justify-center rounded"
@click="minimize"
aria-label="minimize"
>
<IconMinimize />
</button>
<button
class="btn w-7 h-7 flex items-center justify-center rounded"
@click="slideOver"
aria-label="slide over"
>
<IconSplitRight :class="{ 'rotate-180': pipRight < pipLeft }" />
</button>
<button
class="btn w-7 h-7 flex items-center justify-center rounded"
@click="close"
aria-label="close"
>
<IconClose />
</button>
<button
class="btn w-7 h-7 flex items-center justify-center rounded"
@click="refresh"
aria-label="refresh"
>
<IconRefresh />
</button>
<!-- <button @click="">forward</button>
<button @click="">backward</button> -->
</div>
</template>
<style scoped>
.multitasking-menu {
top: 4px;
position: fixed;
left: 50%;
transform: translateX(-50%);
}
.multitasking-menu:hover::before {
backdrop-filter: blur(5px) invert(5%);
}
.multitasking-menu::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
border-radius: 999px;
top: 0;
left: 0;
}
.multitasking-menu:hover dot {
backdrop-filter: invert(50%);
}
.dot {
backdrop-filter: invert(20%);
}
.btn-group {
backdrop-filter: blur(5px) invert(5%);
top: 12px;
border: 1px solid var(--color-border);
color: white;
left: 50%;
transform: translateX(-50%);
background-color: rgba(60, 60, 60, 0.5);
animation: 200ms ease-out fadein;
}
.btn-group .btn {
}
.btn-group .btn:hover {
background: rgba(30, 30, 30, 0.2);
}
@keyframes fadein {
from {
transform: translateX(-50%) translateY(-50%) scale(50%);
opacity: 0.2;
}
to {
transform: translateX(-50%) translateY(0) scale(100%);
opacity: 100;
}
}
</style>
<script setup lang="ts">
import { pipEventName } from "@/content/pip";
import { PipEventName } from "@/types/pip";
import { reactive, ref } from "vue";
import IconClose from "./icons/IconClose.vue";
const emit = defineEmits(["close"]);
......@@ -8,7 +9,7 @@ const host = ref(location.host);
function handleClick() {
document.dispatchEvent(
new CustomEvent(pipEventName, {
new CustomEvent(PipEventName.pip, {
detail: {
url: location.href,
mode: "write-html",
......@@ -45,7 +46,7 @@ function handleClose(e: MouseEvent) {
]"
@click="handleClose"
>
×
<IconClose class="w-4 h-4" />
</div>
</div>
<div class="text-2xl font-bold text-center mt-24 opacity-60">
......
<script setup lang="ts">
import LoadingBar from "./LoadingBar.vue";
</script>
<template>
<div>
<div class="text-2xl font-bold text-center mt-24 opacity-60">
Anything Copilot
</div>
<div class="px-5 mt-12">
<LoadingBar />
</div>
</div>
</template>
<style scoped></style>
<script setup lang="ts"></script>
<template>
<div></div>
</template>
<style scoped></style>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 -960 960 960"
width="24"
fill="currentColor"
>
<path
d="m480-320 160-160-160-160-56 56 64 64H320v80h168l-64 64 56 56Zm0 240q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"
/>
</svg>
</template>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 -960 960 960"
width="24"
fill="currentColor"
>
<path
d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"
/>
</svg>
</template>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 -960 960 960"
width="24"
fill="currentColor"
>
<path
d="m177-120-57-57 184-183H200v-80h240v240h-80v-104L177-120Zm343-400v-240h80v104l183-184 57 57-184 183h104v80H520Z"
/>
</svg>
</template>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 -960 960 960"
width="24"
fill="currentColor"
>
<path d="M240-120v-80h480v80H240Z" />
</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-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z"
/>
</svg>
</template>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 -960 960 960"
width="24"
fill="currentColor"
>
<path
d="M600-120q-33 0-56.5-23.5T520-200v-560q0-33 23.5-56.5T600-840h160q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H600Zm-400 0q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h160q33 0 56.5 23.5T440-760v560q0 33-23.5 56.5T360-120H200Zm0-640v560h160v-560H200Zm160 560H200h160Z"
/>
</svg>
</template>
<script setup lang="ts">
import { contentCss, pipLauncher } from "./store";
import { pipLauncher } from "./store";
import PipLauncher from "@/components/PipLauncher.vue";
</script>
......
<script setup lang="ts">
import Multitasking from "@/components/Multitasking.vue";
import PipSplash from "@/components/PipSplash.vue";
import { pipLoading } from "./store";
import LoadingBar from "@/components/LoadingBar.vue";
</script>
<template>
<Multitasking />
<div
v-if="pipLoading.splashScreen && pipLoading.isLoading"
class="fixed top-0 left-0 w-full h-full"
>
<PipSplash />
</div>
<div
v-if="!pipLoading.splashScreen && pipLoading.isLoading"
class="fixed w-full top-0 left-0 z-[9999]"
>
<LoadingBar />
</div>
</template>
<style scoped></style>
import { pip, pipEventName } from "./pip";
import { waitMountApp } from "./ui";
import { contentCss, pipLauncher } from "./store";
import { mount, waitMountApp } from "./ui";
import {
contentCss,
pipLauncher,
pipLoading,
pipWindowInfo,
pipWindowRef,
} from "./store";
import { MessageType } from "@/types";
import Copilot from "./Copilot.vue";
import { waitMessage } from "@/utils/ext";
import { PipEventName } from "@/types/pip";
function handleMessage(
message: any,
......@@ -9,25 +18,88 @@ function handleMessage(
) {
console.log(message, sender);
switch (message?.type) {
case "pip":
case MessageType.pip:
document.dispatchEvent(
new CustomEvent(pipEventName, { detail: message.options })
new CustomEvent(PipEventName.pip, { detail: message.options })
);
break;
case "content-css":
contentCss.value = message.payload?.value || "";
break;
case "pip-launch":
case MessageType.pipLaunch:
pipLauncher.visible = true;
break;
case "hi-content":
case MessageType.hiContent:
chrome.runtime.sendMessage({
type: "content-here",
type: MessageType.contentHere,
});
sendResponse({ type: MessageType.contentHere });
break;
case MessageType.pipWinInfo:
pipWindowInfo.value = message.window;
chrome.storage.local.set({
pipWindowId: message.window.id,
});
sendResponse({ type: "content-here" });
break;
}
}
chrome.runtime.onMessage.addListener(handleMessage);
async function handlePipEvent(event: any) {
const pipWindow = await new Promise<Window | null>((r) => {
const docPip = window.documentPictureInPicture;
const handleEnter = () => {
r(docPip.window);
docPip?.removeEventListener("enter", handleEnter);
};
docPip?.addEventListener("enter", handleEnter);
});
console.log("content pip event: ", event);
if (pipWindow) {
pipWindowRef.value = pipWindow;
mount(Copilot, pipWindow.document);
await new Promise<void>((r) => {
document.addEventListener(PipEventName.loaded, (e) => {
console.log("load", e);
r();
});
});
// may be 0 if not wait document is loaded
chrome.runtime.sendMessage({
type: MessageType.getPipWinInfo,
options: {
width: pipWindow.outerWidth,
height: pipWindow.outerHeight,
},
});
pipWindow.addEventListener("pagehide", () => {
chrome.storage.local.set({
pipWindowId: undefined,
});
});
}
}
async function handlePipLoadedEvent(e: Event) {
console.log("e: ", e);
pipLoading.splashScreen = false;
pipLoading.isLoading = false;
const pipWindow = window.documentPictureInPicture.window;
if (pipWindow) {
mount(Copilot, pipWindow.document);
}
}
async function handlePopLoadDocEvent(e: CustomEvent | Event) {
pipLoading.isLoading = true;
}
chrome.runtime.onMessage.addListener(handleMessage);
document.addEventListener(PipEventName.pip, handlePipEvent);
document.addEventListener(PipEventName.loaded, handlePipLoadedEvent);
document.addEventListener(PipEventName.loadDoc, handlePopLoadDocEvent);
waitMountApp();
import { pip, pipEventName } from "./pip";
import { PipEventName } from "@/types/pip";
import { copilotNavigateTo, pip } from "./pip";
function handleEvent(event: any) {
function handlePipEvent(event: CustomEvent | Event) {
console.log(event);
if ("detail" in event) {
try {
pip(event.detail);
} catch (e) {
console.error(e);
}
}
}
function handleLoadDocEvent(event: CustomEvent | Event) {
if ("detail" in event) {
copilotNavigateTo(event.detail.url);
}
}
document.addEventListener(pipEventName, handleEvent);
document.addEventListener(PipEventName.pip, handlePipEvent);
document.addEventListener(PipEventName.loadDoc, handleLoadDocEvent);
window.addEventListener("securitypolicyviolation", (e) => {
console.warn(e);
......
import type { PipOptions } from "@/types/pip";
import { PipEventName, type PipOptions } from "@/types/pip";
import {
querySome,
copyStyleSheets,
......@@ -7,12 +7,12 @@ import {
removePrerenderRules,
} from "@/utils/dom";
export const pipEventName = "anything-copilot-pip";
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",
// "User-Agent":
// "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36",
};
return fetch(input, {
...init,
......@@ -23,6 +23,8 @@ function fetchDoc(input: URL | RequestInfo, init?: RequestInit) {
});
}
let initWindow = null as any;
export async function pip(options: PipOptions) {
let { mode, selector, url, isCopyStyle } = options;
url = url || location.href;
......@@ -33,10 +35,14 @@ export async function pip(options: PipOptions) {
}
const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 350,
width: 420,
height: 800,
});
initWindow = { ...pipWindow };
// test(pipWindow.document, pipWindow);
if (isCopyStyle) {
copyStyleSheets(pipWindow, document);
}
......@@ -76,9 +82,9 @@ export async function copilotNavigateTo(url: string) {
const res = await fetchDoc(url);
const html = await res.text();
// resetWindow(pipWindow);
writeHtml(pipWindow, html);
navGuard(pipWindow);
history.replaceState(null, "", url);
pipWindow.history.replaceState(pipWindow.history.state, "", url);
}
type ReopenOptions = {
......@@ -121,6 +127,7 @@ function writeHtml(pipWindow: Window, html: string) {
pipWindow.document.open();
pipWindow.document.write(escaped);
pipWindow.document.close();
document.dispatchEvent(new CustomEvent(PipEventName.loaded));
const base = document.createElement("base");
base.target = "_blank";
......@@ -173,3 +180,53 @@ function navGuard(pipWindow: Window) {
pipWindow.addEventListener("beforeunload", handleBeforeUnload);
pipWindow.addEventListener("click", handleClick);
}
function resetWindow(win: Window) {
// const iframe = document.createElement("iframe");
// iframe.style.display = "none";
// win.document.body.appendChild(iframe);
// const iframeWindow = iframe.contentWindow;
// if (iframeWindow) {
// for (let k of Object.keys(win)) {
// if (!(k in iframeWindow)) {
// delete win[k as any];
// }
// }
// }
Object.keys(win).forEach((k) => {
if (k == "location") return;
try {
win[k as any] = initWindow[k];
} catch (e) {
console.error(e);
}
});
Object.keys(win.document).forEach((k) => {
if (k == "location") return;
try {
win.document[k as "body"] = initWindow.document[k];
} catch (e) {
console.error(e);
}
});
}
function test(doc: Document, win: Window) {
doc.addEventListener("DOMContentLoaded", (e) => {
console.warn("DOMContentLoaded", e);
});
win.addEventListener("beforeunload", (e) => {
console.warn("beforeunload", e);
});
win.addEventListener("load", (e) => {
console.warn("load", e);
});
win.addEventListener("unload", (e) => {
console.warn("unload", e);
});
}
......@@ -28,3 +28,11 @@ export const items = reactive([
title: "Tiktok",
},
]);
export const pipWindowRef = ref<Window | null>(null);
export const pipWindowInfo = ref<chrome.windows.Window | null>(null);
export const pipLoading = reactive({
isLoading: true,
splashScreen: true,
});
import { createApp } from "vue";
import { createApp, type Component } from "vue";
import { createI18n } from "vue-i18n";
import App from "./App.vue";
import { MessageType } from "@/types";
import "@/assets/main.css";
export function mountApp() {
const outter = document.createElement("anything-copilot");
const shadowRoot = outter.attachShadow({ mode: "open" });
const appContainer = document.createElement("div");
const isSelf = chrome.runtime.id === location.host;
export function mount(App: Component, doc = document) {
const outter = doc.createElement("anything-copilot");
const root = isSelf ? outter : outter.attachShadow({ mode: "open" });
const appContainer = doc.createElement("div");
appContainer.id = "app";
const link = document.createElement("link");
const link = doc.createElement("link");
link.rel = "stylesheet";
link.href = chrome.runtime.getURL("/index.css");
shadowRoot.append(link);
shadowRoot.append(appContainer);
document.documentElement.append(outter);
createApp(App).mount(appContainer);
root.append(link);
root.append(appContainer);
doc.documentElement.append(outter);
const app = createApp(App);
const i18n = createI18n({});
app.use(i18n);
app.mount(appContainer);
}
// chrome.runtime.sendMessage({
// type: "get-content-css",
// url: "/index.css",
// });
export function mountApp(doc = document) {
mount(App, doc);
chrome.runtime.sendMessage({ type: MessageType.contentMount });
}
export function waitMountApp() {
......
<script setup lang="ts">
import { ref, onMounted, computed, reactive } from "vue";
import { ref, onMounted, onUnmounted, computed, reactive, watch } from "vue";
import { emptyTab, checkContent } from "@/utils/ext";
import { items } from "@/content/store";
import IconHide from "@/components/icons/IconHide.vue";
import IconArrowCircleRight from "@/components/icons/IconArrowCircleRight.vue";
import IconClose from "@/components/icons/IconClose.vue";
const activeTab = ref<chrome.tabs.Tab>(emptyTab);
const manifest = reactive(chrome.runtime.getManifest());
const avaiable = ref(false);
const pipWindowId = ref(0);
const pipWindowInfo = reactive({
windowId: 0,
isOpen: false,
favIconUrl: "",
title: "",
});
const handleLocalChange = (changes: {
[key: string]: chrome.storage.StorageChange;
}) => {
if (changes.pipWindowId) {
pipWindowId.value = changes.pipWindowId.newValue;
}
};
watch(pipWindowId, async (id) => {
if (id) {
const tabs = await chrome.tabs.query({ windowId: id });
console.log("pip window tabs: ", tabs);
if (tabs && tabs.length == 1) {
pipWindowInfo.isOpen = true;
pipWindowInfo.favIconUrl = tabs[0].favIconUrl || "";
pipWindowInfo.title = tabs[0].title || "";
}
return;
}
pipWindowInfo.isOpen = false;
});
onMounted(() => {
chrome.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
console.log(tabs);
......@@ -16,6 +52,20 @@ onMounted(() => {
avaiable.value = value;
});
});
chrome.storage.local
.get({ pipWindowId: null })
.then(({ pipWindowId: id }) => {
if (id) {
pipWindowId.value = id;
}
});
chrome.storage.local.onChanged.addListener(handleLocalChange);
});
onUnmounted(() => {
chrome.storage.local.onChanged.removeListener(handleLocalChange);
});
const host = computed({
......@@ -48,6 +98,17 @@ async function handleClickLaunch(url: string) {
url,
});
}
async function handleUpdatePip(state: "normal" | "minimized") {
chrome.windows.update(pipWindowId.value, {
state,
});
}
async function closePip() {
await chrome.windows.update(pipWindowId.value, { state: "normal" });
chrome.windows.remove(pipWindowId.value);
}
</script>
<template>
......@@ -57,6 +118,39 @@ async function handleClickLaunch(url: string) {
<span class="mx-2 text-sm opacity-50">{{ manifest.version }}</span>
</div>
<div v-if="pipWindowInfo.isOpen">
<div class="text-sm flex items-center truncate mt-6">
<span
class="w-4 h-4 inline-block mr-2 rounded"
:style="{
background:
'center / contain url(' + pipWindowInfo.favIconUrl + ')',
}"
></span>
<span>{{ pipWindowInfo.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>
<div class="text-sm truncate mt-6">
{{ host }}
<span class="text-rose-800" v-if="!avaiable">{{
......
import '@/assets/main.css'
import "@/assets/main.css";
import { createApp } from 'vue'
import Popup from './Popup.vue'
import { createApp } from "vue";
import { createI18n } from "vue-i18n";
import Popup from "./Popup.vue";
createApp(Popup).mount('#app')
const i18n = createI18n({});
const app = createApp(Popup);
app.use(i18n);
app.mount("#app");
export enum MessageType {
pip = "pip",
hiContent = "hi-content",
contentHere = "content-here",
bgOpenPip = "bg-open-pip",
bgPipLaunch = "bg-pip-launch",
pipLaunch = "pip-launch",
contentMount = "content-mount",
minimizePipWin = "minimize-pip-win",
getPipWinInfo = "get-pip-win-info",
pipWinInfo = "pip-win-info",
updatePipWin = "update-pip-win",
}
......@@ -4,3 +4,9 @@ export type PipOptions = {
mode: "iframe" | "write-html" | "move-element";
isCopyStyle?: boolean;
};
export enum PipEventName {
pip = "anything-copilot-pip",
loaded = "anything-copilot-loaded",
loadDoc = "anything-copilot-load-doc",
}
import { MessageType } from "@/types";
type MessageSender = chrome.runtime.MessageSender;
type UpdatedOption = {
......@@ -23,6 +25,37 @@ export async function tabUpdated({ tabId, status, timeout }: UpdatedOption) {
});
}
type WaitMessageOption = {
type: string;
tabId?: number;
timeout?: number;
};
export async function waitMessage<T = unknown>({
type,
tabId,
timeout,
}: WaitMessageOption): Promise<T> {
return new Promise<T>((r, reject) => {
let timer = 0;
const handleMessage = (message: any, sender: MessageSender) => {
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.addListener(handleMessage);
if (timeout) {
timer = setTimeout(() => {
chrome.runtime.onMessage.removeListener(handleMessage);
reject();
}, timeout);
}
});
}
export const emptyTab: chrome.tabs.Tab = {
active: false,
autoDiscardable: false,
......@@ -54,7 +87,9 @@ export async function checkContent(tabId: number) {
chrome.runtime.onMessage.addListener(handleMessage);
});
const res = await chrome.tabs.sendMessage(tabId, { type: "hi-content" });
const res = await chrome.tabs.sendMessage(tabId, {
type: MessageType.hiContent,
});
alive = !!res;
console.log("hi-content response: ", alive, res);
......
......@@ -3,13 +3,12 @@ import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import svgr from "vite-plugin-svgr";
import manifest from "./manifest";
import makeManifest from "./utils/manifest-plugin";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), vueJsx(), svgr(), makeManifest(manifest, { isDev: false })],
plugins: [vue(), vueJsx(), makeManifest(manifest, { isDev: false })],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
......@@ -17,11 +16,18 @@ export default defineConfig({
},
build: {
emptyOutDir: false,
assetsDir: "assets",
outDir: "dist",
rollupOptions: {
input: {
popup: "popup.html",
guide: "guide.html",
},
output: {
assetFileNames: "[name].[ext]",
chunkFileNames: "[name]-chunk.js",
entryFileNames: "[name].js",
},
},
},
});
......@@ -2,10 +2,12 @@ import { defineConfig } from "vite";
import { fileURLToPath, URL } from "node:url";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import svgr from "vite-plugin-svgr";
export default defineConfig({
plugins: [vue(), vueJsx(), svgr()],
server: {
port: 3000,
},
plugins: [vue(), vueJsx()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
......
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