Commit 31d553e6 authored by Domi's avatar Domi

feat: content sidebar

parent 1d61640e
......@@ -4,11 +4,14 @@
<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>
<base href="http://localhost:3000" />
<title>Anything Copilot DEV</title>
</head>
<body>
<div id="app"></div>
<div>
<div class="h-screen"></div>
<div class="h-screen"></div>
</div>
<script type="module" src="./src/pages/dev.ts"></script>
</body>
</html>
......@@ -12,7 +12,6 @@ const ctx = await esbuild.context({
entryPoints: {
// 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,
......
......@@ -98,9 +98,9 @@
"icon": "https://r2.ziziyi.com/copilot/gemini.svg"
},
{
"url": "https://claude.ai/",
"title": "Claude",
"icon": "https://r2.ziziyi.com/copilot/claude-ai.svg"
"url": "https://runwayml.com/ai-tools/text-to-image/",
"title": "Text to Image",
"icon": "https://r2.ziziyi.com/copilot/runway.png"
}
]
}
......
......@@ -6,6 +6,11 @@ import {
} from "@/types"
import { waitMessage, tabUpdated, getLocal } from "@/utils/ext"
import { offscreen } from "./offscreen"
import {
registerContentSidebar,
unregisterContentSidebar,
handleContentMounted,
} from "./sidebar"
import config from "@/assets/config.json"
import { allFrameScript, mainContentScript } from "@/manifest"
......@@ -22,6 +27,11 @@ const contentScript = {
} satisfies chrome.scripting.RegisteredContentScript
chrome.scripting.registerContentScripts([contentScript, mainContentScript])
chrome.runtime.onMessage.addListener(handleMessage)
chrome.commands.onCommand.addListener(handleCommand)
chrome.runtime.onStartup.addListener(() => {
updateConfig()
})
async function openPipBackground(url: string) {
const tab = await chrome.tabs.create({
......@@ -43,7 +53,7 @@ async function pipLaunch(url: string) {
const tab = await chrome.tabs.create({ url })
await waitMessage({
tabId: tab.id!,
type: MessageType.contentMount,
type: MessageType.contentMounted,
})
chrome.tabs.sendMessage(tab.id!, {
type: MessageType.pipLaunch,
......@@ -150,11 +160,18 @@ function handleMessage(message: any, sender: chrome.runtime.MessageSender) {
case MessageType.invokeRequest:
handleInvokeRequest(message, sender)
break
case MessageType.contentMounted:
handleContentMounted(sender.tab!.id!)
break
case MessageType.registerContentSidebar:
registerContentSidebar({ tabId: sender.tab!.id!, url: message.url })
break
case MessageType.unregisterContentSidebar:
unregisterContentSidebar(sender.tab!.id!)
break
}
}
chrome.runtime.onMessage.addListener(handleMessage)
async function handleToggleMinimize() {
const { pipWindowId } = await chrome.storage.local.get({ pipWindowId: null })
if (!pipWindowId) return
......@@ -174,8 +191,6 @@ function handleCommand(command: string) {
}
}
chrome.commands.onCommand.addListener(handleCommand)
async function updateConfig() {
const url = "https://config.ziziyi.com/anything-copilot"
const now = Date.now()
......@@ -235,7 +250,3 @@ async function updateConfig() {
loadCandidates,
})
}
chrome.runtime.onStartup.addListener(() => {
updateConfig()
})
import { MessageType } from "@/types"
type ContentSidebar = {
tabId: number
url?: string
}
const contentSidebarMap = new Map<number, ContentSidebar>()
export function registerContentSidebar({ tabId, url }: ContentSidebar) {
contentSidebarMap.set(tabId, { tabId, url })
}
export function unregisterContentSidebar(tabId: number) {
contentSidebarMap.delete(tabId)
}
export async function handleContentMounted(tabId: number) {
const sidebar = contentSidebarMap.get(tabId)
if (sidebar) {
await chrome.storage.session.set({
sidebarUrls: { content: sidebar.url },
})
chrome.tabs.sendMessage(tabId, {
type: MessageType.openContentSidebar,
url: sidebar.url,
})
}
}
<script setup lang="ts">
import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue"
import IconAmpStories from "@/components/icons/IconAmpStories.vue"
import IconClose from "./icons/IconClose.vue"
defineProps<{
icon: string
title: string
badge?: "popup" | "sidebar" | "remove"
small?: boolean
}>()
defineEmits(["click", "remove"])
</script>
<template>
<button
:class="[
'group shrink-0 relative flex flex-col items-center justify-self-center rounded-lg p-1 bg-background-soft hover:bg-background-mute',
small ? 'w-[58px]' : 'w-16',
]"
@click="$emit('click')"
>
<span
class="size-6 rounded mt-2 mb-1"
:style="{
background: 'center / contain url(' + icon + ')',
}"
>
</span>
<div class="flex flex-col justify-center h-6 my-1">
<div class="text-xs max-w-full break-words leading-3 line-clamp-2">
{{ title }}
</div>
</div>
<IconSplitscreenRight
v-if="badge === 'sidebar'"
class="size-3 absolute top-1 right-1 opacity-0 group-hover:opacity-65"
/>
<IconAmpStories
v-if="badge === 'popup'"
class="size-3 absolute top-1 right-1 opacity-0 group-hover:opacity-65"
/>
<div
v-if="badge === 'remove'"
:class="[
'size-4 absolute top-[-4px] right-[-4px] opacity-0 group-hover:opacity-65 rounded-full',
'bg-background-soft hover:text-red-500 flex items-center justify-center',
]"
@click="
(e) => {
e.stopPropagation()
e.preventDefault()
$emit('remove')
}
"
>
<IconClose class="size-3" />
</div>
</button>
</template>
<style scoped></style>
<script setup lang="ts">
defineProps<{
image?: string;
title: string;
}>();
</script>
<template>
<button class="primary-btn w-full flex items-center mt-2 rounded-lg p-2">
<span
class="w-6 h-6 inline-block mr-2 rounded"
:style="{
background: 'center / contain url(' + image + ')',
}"
>
</span>
<span class="text-base font-bold">{{ title }}</span>
</button>
</template>
<style scoped></style>
<script setup lang="ts"></script>
<script setup lang="ts">
import {} from "vue"
</script>
<template>
<div></div>
......
......@@ -10,6 +10,10 @@ const props = defineProps<{
url: string
}>()
const emit = defineEmits<{
pageInfo: [pageInfo: { url: string; title: string; icon: string }]
}>()
const frame = ref<HTMLIFrameElement>()
const patchs = reactive(config.data.webviewPatchs)
const loadUrls = reactive(config.data.loadCandidates)
......@@ -32,23 +36,7 @@ function handleFrameMessage(e: MessageEvent) {
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 })
})
emit("pageInfo", pageInfo)
}
break
}
......@@ -118,7 +106,7 @@ watch(patch, async (patch) => {
pageInfo.url = ""
pageInfo.title = ""
pageInfo.icon = ""
const loadTimeout = 1000 * 1
const loadTimeout = 1000 * 100
try {
await new Promise<void>((resolve, reject) => {
......
......@@ -10,6 +10,7 @@ import { getDocItem, devConfig, type SiteConfig } from "./helper"
import config from "@/assets/config.json"
import { getLocal } from "@/utils/ext"
import { chatDocPrompt } from "@/utils/prompt"
import { autoPointerCapture } from "@/utils/dom"
const { t } = useI18n()
const logoUrl = chrome.runtime.getURL("/logo.svg")
......@@ -188,8 +189,7 @@ onUnmounted(() => {
>
<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)
"
@pointerdown="autoPointerCapture"
@pointermove="handlePointerMove"
@pointerup="adjustPosition"
@pointercancel="adjustPosition"
......
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 -960 960 960"
width="24"
fill="currentColor"
>
<path
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm440-80h120v-560H640v560Zm-80 0v-560H200v560h360Zm80 0h120-120Z"
/>
</svg>
</template>
<script setup lang="ts">
import { debounce } from "lodash-es"
import { onMounted, onUnmounted, ref, watch } from "vue"
import { getLocal } from "@/utils/ext"
import IconClose from "@/components/icons/IconClose.vue"
import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue"
import PageScrollbar from "./PageScrollbar.vue"
import { MessageType } from "@/types"
import { autoPointerCapture } from "@/utils/dom"
const props = defineProps<{
hidden?: boolean
}>()
const emit = defineEmits(["close", "hide"])
const sidebarHtml = chrome.runtime.getURL("sidebar.html")
const sidebarUrl = sidebarHtml + "?mode=content"
const logoUrl = chrome.runtime.getURL("/logo.svg")
let sheet: CSSStyleSheet | null = null
let invisibleTimer = 0
const width = ref(360)
const invisible = ref(false)
const pointermove = (e: PointerEvent) => {
if (e.buttons) {
width.value -= e.movementX
}
}
function updatePageStyle(width = 0) {
if (!sheet) {
sheet = new CSSStyleSheet()
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]
}
const style =
`html::-webkit-scrollbar { display: none; } ` +
`html { max-width: calc(100% - ${width}px); }`
sheet.replace(style)
sheet.disabled = false
}
function disablePageStyle() {
if (sheet) {
sheet.disabled = true
}
}
function handleKeydown(e: KeyboardEvent) {
// console.log(e.key, e)
if (e.key == "Escape" && !invisible.value) {
invisibleTimer = window.setTimeout(() => {
invisible.value = true
}, 300)
}
}
function handleKeyUp(e: KeyboardEvent) {
if (e.key == "Escape") {
window.clearTimeout(invisibleTimer)
invisible.value = false
}
}
watch(
width,
debounce((value) => {
updatePageStyle(value)
chrome.storage.local.set({
contentSidebarWidth: value,
})
}, 300)
)
watch(
() => props.hidden,
() => {
if (props.hidden) {
disablePageStyle()
} else {
updatePageStyle(width.value)
}
}
)
onMounted(() => {
getLocal({ contentSidebarWidth: 360 }).then((data) => {
width.value = data.contentSidebarWidth
})
updatePageStyle(width.value)
window.addEventListener("keydown", handleKeydown)
window.addEventListener("keyup", handleKeyUp)
chrome.runtime.sendMessage({
type: MessageType.registerContentSidebar,
})
})
onUnmounted(() => {
disablePageStyle()
window.removeEventListener("keydown", handleKeydown)
window.removeEventListener("keyup", handleKeyUp)
chrome.runtime.sendMessage({
type: MessageType.unregisterContentSidebar,
})
})
</script>
<template>
<div
:class="[
'fixed flex right-0 top-0 h-screen min-w-80 max-w-[80vw] z-[999999]',
{ invisible: invisible || props.hidden },
]"
:style="{ width: `${width}px` }"
>
<PageScrollbar class="ml-[-2px]" />
<div class="relative flex-1 flex flex-col bg-background">
<div
class="absolute h-full w-1 top-0 left-[-2px] cursor-ew-resize"
@pointerdown="autoPointerCapture"
@pointermove="pointermove"
></div>
<div class="flex gap-2 items-center justify-between h-9 px-2">
<button
@click=""
class="size-6 flex items-center justify-center mr-auto"
>
<img class="size-4" :src="logoUrl" />
</button>
<button
@click="emit('hide')"
class="size-6 rounded-full hover:bg-background-soft flex items-center justify-center"
>
<IconSplitscreenRight class="size-4 scale-95" />
</button>
<button
@click="emit('close')"
class="size-6 rounded-full hover:bg-background-soft flex items-center justify-center"
>
<IconClose class="size-4" />
</button>
</div>
<iframe class="w-full h-full flex-1" :src="sidebarUrl"></iframe>
</div>
</div>
</template>
<style scoped></style>
<script setup lang="ts">
import { ref } from "vue"
import ContentSidebar from "./ContentSidebar.vue"
import { sidebarAddon } from "@/store"
import { autoPointerCapture } from "@/utils/dom"
const logoUrl = chrome.runtime.getURL("/logo.svg")
const top = ref(72)
function handlePointerMove(e: PointerEvent) {
if (e.buttons) {
const value = top.value + e.movementY
top.value = Math.max(12, Math.min(window.innerHeight - 60, value))
}
}
</script>
<template>
<ContentSidebar
v-if="sidebarAddon.visible"
:hidden="sidebarAddon.hidden"
@close="sidebarAddon.visible = false"
@hide="sidebarAddon.hidden = true"
/>
<div
v-if="sidebarAddon.hidden"
:class="[
'fixed top-16 right-0 p-1.5 bg-background/65 backdrop-blur-md cursor-pointer',
'rounded-s shadow-lg z-[999999] hover:bg-background-mute/65',
]"
:style="{ top: top + 'px' }"
@pointerdown="autoPointerCapture"
@pointermove="handlePointerMove"
@click="sidebarAddon.hidden = false"
>
<img :src="logoUrl" class="size-4 pointer-events-none select-none" />
</div>
</template>
<style scoped></style>
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, ref } from "vue"
const props = defineProps<{
class?: string
}>()
const scrollEl = ref<HTMLElement>()
const barWidth = ref(18)
const height = ref(0)
const top = ref(0)
const html = document.documentElement
let updateAt = 0
let syncAt = 0
let operate = "sync"
onMounted(() => {
const { scrollHeight, scrollTop } = html
height.value = scrollHeight
top.value = scrollTop
nextTick(() => {
if (scrollEl.value) {
barWidth.value += 1 - scrollEl.value.clientWidth
}
})
document.addEventListener("scroll", handleScroll)
})
onUnmounted(() => {
document.removeEventListener("scroll", handleScroll)
})
function updateScroll(e: UIEvent) {
updateAt = e.timeStamp
const el = e.target as HTMLElement
if (updateAt - syncAt > 200) {
operate = "update"
}
if (operate == "update") {
// html.scrollTop = el.scrollTop
html.scrollTo({ top: el.scrollTop, behavior: "instant" })
}
}
function handleScroll(e: Event) {
syncAt = e.timeStamp
const { scrollHeight, scrollTop } = html
height.value = scrollHeight
top.value = scrollTop
const bar = scrollEl.value
if (!bar) return
if (syncAt - updateAt > 200) {
operate = "sync"
barWidth.value += 1 - bar.clientWidth
}
if (scrollEl.value && operate == "sync") {
scrollEl.value.scrollTop = scrollTop
}
}
</script>
<template>
<div
ref="scrollEl"
:class="['h-full overflow-y-auto', props.class]"
:style="{ width: barWidth + 'px' }"
@scroll="updateScroll"
>
<div class="w-0" :style="{ height: height + 'px' }"></div>
</div>
</template>
<style scoped></style>
<script setup lang="ts">
import { pipLauncher } from "@/store"
import PipLauncher from "@/components/PipLauncher.vue"
import { MessageType } from "@/types"
import { useI18n } from "vue-i18n"
import ChatDocsAddon from "@/components/chatdocs/ChatDocsAddon.vue"
import ContentSidebarAddon from "@/components/sidebar/ContentSidebarAddon.vue"
import { onMounted } from "vue"
const { t } = useI18n()
const topFrame = window.parent == window
onMounted(() => {
chrome.runtime?.sendMessage({ type: MessageType.contentMounted })
})
</script>
<template>
......@@ -15,4 +23,5 @@ const { t } = useI18n()
/>
<ChatDocsAddon />
</template>
\ No newline at end of file
<ContentSidebarAddon v-if="topFrame" />
</template>
import { MessageType } from "@/types"
chrome.runtime.sendMessage({
type: MessageType.frameReady,
})
import { mount, waitMountApp } from "./ui"
import { chatDocsPanel, pipLauncher, pipLoading, pipWindow } from "@/store"
import {
chatDocsPanel,
pipLauncher,
pipLoading,
pipWindow,
sidebarAddon,
} from "@/store"
import { ContentEventType, FrameMessageType, MessageType } from "@/types"
import Copilot from "./Copilot.vue"
import { waitMessage } from "@/utils/ext"
......@@ -46,6 +52,11 @@ function handleMessage(
case MessageType.showChatDocs:
chatDocsPanel.visible = true
break
case MessageType.openContentSidebar:
sidebarAddon.visible = true
sidebarAddon.hidden = false
sidebarAddon.url = message.url
break
}
}
......@@ -115,7 +126,7 @@ function run() {
async function postPageInfo() {
await new Promise((r) => setTimeout(r, 1000 * 3))
window.top?.postMessage(
window.parent?.postMessage(
{
type: FrameMessageType.pageInfo,
url: location.href,
......@@ -126,27 +137,30 @@ async function postPageInfo() {
)
}
if (window.self == window.top) {
function handleFrameMessage(e: MessageEvent) {
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 },
})
}
}
if (window.self == window.parent) {
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(
window.addEventListener("message", handleFrameMessage)
window.parent?.postMessage(
{
type: FrameMessageType.frameReady,
url: location.href,
},
chrome.runtime.getURL("")
)
......
......@@ -33,7 +33,6 @@ export function mount(App: Component, doc = document) {
export function mountApp(doc = document) {
mount(App, doc)
chrome.runtime?.sendMessage({ type: MessageType.contentMount })
}
export function waitMountApp() {
......
......@@ -42,5 +42,6 @@
"autoSending": "እንደሚቆጠር መላክ",
"chooseContentRelevant": "ከመረጡ የሚያሳውቁ ተግባራዎችን ለመረጡ ይችላሉ",
"notSupported": "ይህ ገጽ ራስ-ሰር መላክን አይደግፍም. እባክዎ መልዕክቱን ይቅዱ እና እራስዎ ይላኩ."
}
},
"openSidebar": "የጎን አሞሌውን ይክፈቱ"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "الإرسال التلقائي",
"chooseContentRelevant": "اختر محتوى أكثر صلة بالموضوع الذي ترغب في التعلم عنه",
"notSupported": "هذه الصفحة لا تدعم الإرسال التلقائي. يرجى نسخ الرسالة وإرسالها يدويًا."
}
},
"openSidebar": "افتح الشريط الجانبي"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Автоматично изпращане",
"chooseContentRelevant": "Изберете съдържание, свързано с темата, за която искате да научите повече",
"notSupported": "Тази страница не поддържа автоматично изпращане. Моля, копирайте съобщението и го изпратете ръчно."
}
},
"openSidebar": "Отворете страничната лента"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "অটো প্রেরণ",
"chooseContentRelevant": "আপনি যে বিষয়ে আরও জানতে চান তা সম্পর্কিত কনটেন্ট চয়ন করুন",
"notSupported": "এই পৃষ্ঠাটি স্বয়ংক্রিয় প্রেরণকে সমর্থন করে না। দয়া করে বার্তাটি অনুলিপি করুন এবং এটি ম্যানুয়ালি প্রেরণ করুন।"
}
},
"openSidebar": "সাইডবারটি খুলুন"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Enviament automàtic",
"chooseContentRelevant": "Trieu contingut més rellevant pel tema que voleu aprendre",
"notSupported": "Aquesta pàgina no admet l'enviament automàtic. Copieu el missatge i envieu -lo manualment."
}
},
"openSidebar": "Obriu la barra lateral"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Automatické odesílání",
"chooseContentRelevant": "Vyberte obsah více relevantní k tématu, které chcete studovat",
"notSupported": "Tato stránka nepodporuje automatické odesílání. Zkopírujte zprávu a odešlete ji ručně."
}
},
"openSidebar": "Otevřete postranní panel"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Auto afsendelse",
"chooseContentRelevant": "Vælg indhold mere relevant for det emne, du ønsker at lære om",
"notSupported": "Denne side understøtter ikke automatisk afsendelse. Kopier meddelelsen og send den manuelt."
}
},
"openSidebar": "Åbn sidebjælken"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Automatisches Senden",
"chooseContentRelevant": "Wählen Sie Inhalte, die zum gewünschten Thema passen",
"notSupported": "Diese Seite unterstützt das automatische Senden nicht. Bitte kopieren Sie die Nachricht und senden Sie sie manuell."
}
},
"openSidebar": "Öffnen Sie die Seitenleiste"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Αυτόματη αποστολή",
"chooseContentRelevant": "Επιλέξτε περιεχόμενο που σχετίζεται περισσότερο με το θέμα που θέλετε να μάθετε",
"notSupported": "Αυτή η σελίδα δεν υποστηρίζει αυτόματη αποστολή. Αντιγράψτε το μήνυμα και στείλτε το χειροκίνητα."
}
},
"openSidebar": "Ανοίξτε την πλαϊνή μπάρα"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Auto Sending",
"chooseContentRelevant": "Choose content more relevant to the topic you want to learn about",
"notSupported": "This page does not support automatic sending. Please copy the message and send it manually."
}
},
"openSidebar": "Open Sidebar"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Envío Automático",
"chooseContentRelevant": "Elige contenido más relevante para el tema que deseas aprender",
"notSupported": "Esta página no admite el envío automático. Copie el mensaje y envíelo manualmente."
}
},
"openSidebar": "Abrir la barra lateral"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Envío Automático",
"chooseContentRelevant": "Elige contenido más relevante para el tema que deseas aprender",
"notSupported": "Esta página no admite el envío automático. Copie el mensaje y envíelo manualmente."
}
},
"openSidebar": "Abrir la barra lateral"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Automaatne Saatmine",
"chooseContentRelevant": "Valige teema kohta rohkem seotud sisu",
"notSupported": "See leht ei toeta automaatset saatmist. Kopeerige sõnum ja saatke see käsitsi."
}
},
"openSidebar": "Avage külgriba"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "ارسال خودکار",
"chooseContentRelevant": "محتوای مرتبط با موضوعی که می‌خواهید درباره آن یاد بگیرید را انتخاب کنید",
"notSupported": "این صفحه از ارسال خودکار پشتیبانی نمی کند. لطفا پیام را کپی کرده و به صورت دستی ارسال کنید."
}
},
"openSidebar": "نوار کناری را باز کنید"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Automaattilähetys",
"chooseContentRelevant": "Valitse aiheeseesi liittyvämpi sisältö",
"notSupported": "Tämä sivu ei tue automaattista lähettämistä. Kopioi viesti ja lähetä se manuaalisesti."
}
},
"openSidebar": "Avaa sivupalkki"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Auto Padala",
"chooseContentRelevant": "Pumili ng nilalaman na mas kaugnay sa paksa na nais mong malaman",
"notSupported": "Ang pahinang ito ay hindi sumusuporta sa awtomatikong pagpapadala. Mangyaring kopyahin ang mensahe at manu -manong ipadala ito."
}
},
"openSidebar": "Buksan ang sidebar"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Envoi automatique",
"chooseContentRelevant": "Choisissez un contenu plus pertinent pour le sujet que vous souhaitez apprendre",
"notSupported": "Cette page ne prend pas en charge l'envoi automatique. Veuillez copier le message et l'envoyer manuellement."
}
},
"openSidebar": "Ouvrir la barre latérale"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "આપતી મોકલવું",
"chooseContentRelevant": "તમારા શીખવાના વિષય સાથે સંબંધિત કન્ટેન્ટ પસંદ કરો",
"notSupported": "આ પૃષ્ઠ સ્વચાલિત મોકલવાનું સમર્થન કરતું નથી. કૃપા કરીને સંદેશની નકલ કરો અને તેને જાતે મોકલો."
}
},
"openSidebar": "સાઇડબાર ખોલો"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "שליחה אוטומטית",
"chooseContentRelevant": "בחר תוכן הקשור יותר לנושא שברצונך ללמוד עליו",
"notSupported": "דף זה אינו תומך בשליחה אוטומטית. אנא העתק את ההודעה ושלח אותה ידנית."
}
},
"openSidebar": "פתח את סרגל הצד"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "आत्म-भेजन",
"chooseContentRelevant": "उस विषय के बारे में सीखना जिस पर आप चर्चा करना चाहते हैं, उससे संबंधित सामग्री चुनें",
"notSupported": "यह पृष्ठ स्वचालित भेजने का समर्थन नहीं करता है। कृपया संदेश कॉपी करें और इसे मैन्युअल रूप से भेजें।"
}
},
"openSidebar": "साइडबार खोलें"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Automatsko slanje",
"chooseContentRelevant": "Odaberite sadržaj koji je relevantan za temu koju želite naučiti",
"notSupported": "Ova stranica ne podržava automatsko slanje. Kopirajte poruku i pošaljite je ručno."
}
},
"openSidebar": "Otvorite bočnu traku"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Automatikus küldés",
"chooseContentRelevant": "Válassza ki a témához relevánsabb tartalmat, amiről szeretne tanulni",
"notSupported": "Ez az oldal nem támogatja az automatikus küldéseket. Kérjük, másolja az üzenetet, és küldje el manuálisan."
}
},
"openSidebar": "Nyissa ki az oldalsávot"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Pengiriman Otomatis",
"chooseContentRelevant": "Pilih konten yang lebih relevan dengan topik yang ingin Anda pelajari",
"notSupported": "Halaman ini tidak mendukung pengiriman otomatis. Harap salin pesan dan kirimkan secara manual."
}
},
"openSidebar": "Buka bilah samping"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Invio automatico",
"chooseContentRelevant": "Scegli contenuti più pertinenti all'argomento che vuoi apprendere",
"notSupported": "Questa pagina non supporta l'invio automatico. Si prega di copiare il messaggio e inviarlo manualmente."
}
},
"openSidebar": "Apri la barra laterale"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "自動送信",
"chooseContentRelevant": "学びたいトピックに関連するコンテンツを選択してください",
"notSupported": "このページは、自動送信をサポートしていません。メッセージをコピーして手動で送信してください。"
}
},
"openSidebar": "サイドバーを開きます"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "ಸ್ವಯಂ ಕಳುಹಿಸುತ್ತಿದೆ",
"chooseContentRelevant": "ನೀವು ಕಲಿಯಬಯಸುವ ವಿಷಯಕ್ಕೆ ಹೆಚ್ಚಿನ ಸಂಬಂಧಪಟ್ಟ ವಿಷಯಗಳನ್ನು ಆರಿಸಿ",
"notSupported": "ಈ ಪುಟವು ಸ್ವಯಂಚಾಲಿತ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಬೆಂಬಲಿಸುವುದಿಲ್ಲ. ದಯವಿಟ್ಟು ಸಂದೇಶವನ್ನು ನಕಲಿಸಿ ಮತ್ತು ಅದನ್ನು ಕೈಯಾರೆ ಕಳುಹಿಸಿ."
}
},
"openSidebar": "ಸೈಡ್‌ಬಾರ್ ತೆರೆಯಿರಿ"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "자동 전송",
"chooseContentRelevant": "학습하고 싶은 주제와 관련된 콘텐츠를 선택하세요",
"notSupported": "이 페이지는 자동 전송을 지원하지 않습니다. 메시지를 복사하여 수동으로 보내주십시오."
}
},
"openSidebar": "사이드 바를 엽니 다"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Automatinis siuntimas",
"chooseContentRelevant": "Pasirinkite turinį, kuris yra labiau susijęs su jumis dominančia tema",
"notSupported": "Šis puslapis nepalaiko automatinio siuntimo. Nukopijuokite pranešimą ir atsiųskite jį rankiniu būdu."
}
},
"openSidebar": "Atidarykite šoninę juostą"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Automātiska sūtīšana",
"chooseContentRelevant": "Izvēlieties saturu, kas ir saistīts ar tēmu, par kuru vēlaties uzzināt",
"notSupported": "Šī lapa neatbalsta automātisku sūtīšanu. Lūdzu, nokopējiet ziņojumu un nosūtiet to manuāli."
}
},
"openSidebar": "Atveriet sānjoslu"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "ഓട്ടോ അയയ്ക്കൽ",
"chooseContentRelevant": "നിങ്ങളുടെ അറിവിനായി കരുതോട്ട വിഷയത്തിനു കൂടുതൽ ബന്ധമായ ഉള്ളടക്കം തിരഞ്ഞെടുക്കുക",
"notSupported": "ഈ പേജ് യാന്ത്രിക അയയ്ക്കുന്നതിനെ പിന്തുണയ്ക്കുന്നില്ല. സന്ദേശം പകർത്തി സ്വമേധയാ അയയ്ക്കുക."
}
},
"openSidebar": "സൈഡ്ബാർ തുറക്കുക"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "स्वत: पाठवणे",
"chooseContentRelevant": "तुम्हाला ओळखायचं विषयसंबंधित आशय निवडा",
"notSupported": "हे पृष्ठ स्वयंचलित पाठविण्यास समर्थन देत नाही. कृपया संदेश कॉपी करा आणि तो व्यक्तिचलितपणे पाठवा."
}
},
"openSidebar": "साइडबार उघडा"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Penghantaran Automatik",
"chooseContentRelevant": "Pilih kandungan yang lebih berkaitan dengan topik yang anda ingin ketahui",
"notSupported": "Halaman ini tidak menyokong penghantaran automatik. Sila salin mesej dan hantarkan secara manual."
}
},
"openSidebar": "Buka bar sisi"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Automatisch verzenden",
"chooseContentRelevant": "Kies inhoud die relevanter is voor het onderwerp dat je wilt leren",
"notSupported": "Deze pagina ondersteunt geen automatisch verzenden. Kopieer het bericht en stuur het handmatig."
}
},
"openSidebar": "Open de zijbalk"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Auto Sending",
"chooseContentRelevant": "Velg innhold som er mer relevant for emnet du vil lære om",
"notSupported": "Denne siden støtter ikke automatisk sending. Kopier meldingen og send den manuelt."
}
},
"openSidebar": "Åpne sidefeltet"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Automatyczne Wysyłanie",
"chooseContentRelevant": "Wybierz treść bardziej związana z tematem, który chcesz się dowiedzieć",
"notSupported": "Ta strona nie obsługuje automatycznego wysyłania. Skopiuj wiadomość i wysyłaj ją ręcznie."
}
},
"openSidebar": "Otwórz pasek boczny"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Envio Automático",
"chooseContentRelevant": "Escolha conteúdo mais relevante para o tópico que você deseja aprender",
"notSupported": "Esta página não suporta o envio automático. Copie a mensagem e envie -a manualmente."
}
},
"openSidebar": "Abra a barra lateral"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Envio Automático",
"chooseContentRelevant": "Escolha conteúdo mais relevante para o tópico que deseja aprender",
"notSupported": "Esta página não suporta o envio automático. Copie a mensagem e envie -a manualmente."
}
},
"openSidebar": "Abra a barra lateral"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Trimitere Automată",
"chooseContentRelevant": "Alegeți conținut mai relevant pentru subiectul pe care doriți să îl învățați",
"notSupported": "Această pagină nu acceptă trimiterea automată. Vă rugăm să copiați mesajul și să -l trimiteți manual."
}
},
"openSidebar": "Deschideți bara laterală"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Автоматическая Отправка",
"chooseContentRelevant": "Выберите более релевантный контент по теме, которую вы хотите изучить",
"notSupported": "Эта страница не поддерживает автоматическую отправку. Пожалуйста, скопируйте сообщение и отправьте его вручную."
}
},
"openSidebar": "Откройте боковую панель"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Automatické Odosielanie",
"chooseContentRelevant": "Vyberte obsah, ktorý je viac relevantný pre tému, ktorú chcete študovať",
"notSupported": "Táto stránka nepodporuje automatické odosielanie. Skopírujte správu a pošlite ju manuálne."
}
},
"openSidebar": "Otvorte bočný panel"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Avtomatsko Pošiljanje",
"chooseContentRelevant": "Izberite vsebino, ki je bolj relevantna za temo, ki se je želite naučiti",
"notSupported": "Ta stran ne podpira samodejnega pošiljanja. Kopirajte sporočilo in ga pošljite ročno."
}
},
"openSidebar": "Odprite stransko vrstico"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Automatsko Slanje",
"chooseContentRelevant": "Izaberite sadržaj koji je relevantniji za temu koju želite naučiti",
"notSupported": "Ова страница не подржава аутоматско слање. Копирајте поруку и пошаљите га ручно."
}
},
"openSidebar": "Отвори бочну траку"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Automatisk sändning",
"chooseContentRelevant": "Välj innehåll som är mer relevant för det ämne du vill lära dig om",
"notSupported": "Denna sida stöder inte automatisk sändning. Kopiera meddelandet och skicka det manuellt."
}
},
"openSidebar": "Öppna sidofältet"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Kutuma Kiotomatiki",
"chooseContentRelevant": "Chagua yaliyomo inayohusiana zaidi na mada unayotaka kujifunza kuhusu",
"notSupported": "Ukurasa huu hauungi mkono kutuma moja kwa moja. Tafadhali nakili ujumbe na utumie kwa mikono."
}
},
"openSidebar": "Fungua pembeni"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "தானாக அனுப்புதல்",
"chooseContentRelevant": "நீங்கள் அறிந்திருக்க விரும்பும் பகுதிக்கு உரையாடல் தேர்ந்தெடுக்கவும்",
"notSupported": "இந்த பக்கம் தானியங்கி அனுப்புதலை ஆதரிக்காது. தயவுசெய்து செய்தியை நகலெடுத்து கைமுறையாக அனுப்புங்கள்."
}
},
"openSidebar": "பக்கப்பட்டியைத் திறக்கவும்"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "స్వీయం పంపిణీ",
"chooseContentRelevant": "మీరు కలిగిన విషయానికి అనుసంధానం కలిగిన కంటెంట్ ఎంచుకోండి",
"notSupported": "ఈ పేజీ ఆటోమేటిక్ పంపడానికి మద్దతు ఇవ్వదు. దయచేసి సందేశాన్ని కాపీ చేసి మానవీయంగా పంపండి."
}
},
"openSidebar": "సైడ్‌బార్ తెరవండి"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "การส่งอัตโนมัติ",
"chooseContentRelevant": "เลือกเนื้อหาที่เกี่ยวข้องมากขึ้นกับหัวข้อที่คุณต้องการเรียนรู้",
"notSupported": "หน้านี้ไม่รองรับการส่งอัตโนมัติ กรุณาคัดลอกข้อความและส่งด้วยตนเอง"
}
},
"openSidebar": "เปิดแถบด้านข้าง"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Otomatik Gönderim",
"chooseContentRelevant": "Öğrenmek istediğiniz konuyla daha ilgili içerik seçin",
"notSupported": "Bu sayfa otomatik göndermeyi desteklemez. Lütfen mesajı kopyalayın ve manuel olarak gönderin."
}
},
"openSidebar": "Kenar çubuğunu aç"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Автоматичне відправлення",
"chooseContentRelevant": "Виберіть вміст, який більше відповідає темі, яку ви хочете вивчити",
"notSupported": "Ця сторінка не підтримує автоматичне надсилання. Будь ласка, скопіюйте повідомлення та надішліть його вручну."
}
},
"openSidebar": "Відкрийте бічну панель"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "Tự động Gửi",
"chooseContentRelevant": "Chọn nội dung liên quan hơn đến chủ đề bạn muốn tìm hiểu",
"notSupported": "Trang này không hỗ trợ gửi tự động. Vui lòng sao chép tin nhắn và gửi thủ công."
}
},
"openSidebar": "Mở thanh bên"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "自动发送",
"chooseContentRelevant": "选择与你想了解的主题更相关的内容",
"notSupported": "此页面不支持自动发送,请复制消息发送"
}
},
"openSidebar": "打开侧边栏"
}
\ No newline at end of file
......@@ -42,5 +42,6 @@
"autoSending": "自動發送",
"chooseContentRelevant": "選擇與你想了解的主題更相關的內容",
"notSupported": "此頁面不支持自動發送。請複制消息並手動發送。"
}
},
"openSidebar": "打開側邊欄"
}
\ No newline at end of file
......@@ -77,7 +77,6 @@ const manifest = {
"sidePanel",
"declarativeNetRequestWithHostAccess",
"declarativeNetRequestFeedback",
"webNavigation",
],
optional_permissions: [],
host_permissions: ["<all_urls>"],
......@@ -93,11 +92,7 @@ const manifest = {
},
web_accessible_resources: [
{
resources: ["logo.svg"],
matches: ["<all_urls>"],
},
{
resources: ["/js/*", "/assets/*"],
resources: ["/js/*", "/assets/*", "sidebar.html", "logo.svg"],
matches: ["<all_urls>"],
use_dynamic_url: true,
},
......@@ -107,6 +102,6 @@ const manifest = {
? `script-src 'self' http://localhost:3000 'wasm-unsafe-eval';`
: `script-src 'self' 'wasm-unsafe-eval'`,
},
} satisfies Manifest as chrome.runtime.Manifest
} satisfies Manifest as unknown as chrome.runtime.Manifest
export default manifest
<script setup lang="ts">
import ContentSidebar from "@/components/sidebar/ContentSidebar.vue"
</script>
<template>
<ContentSidebar />
</template>
<style scoped></style>
......@@ -19,17 +19,18 @@ import IconAmpStories from "@/components/icons/IconAmpStories.vue"
import IconGppMaybe from "@/components/icons/IconGppMaybe.vue"
import IconHide from "@/components/icons/IconHide.vue"
import IconArrowCircleRight from "@/components/icons/IconArrowCircleRight.vue"
import IconClose from "@/components/icons/IconClose.vue"
import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue"
import SiteButton from "@/components/SiteButton.vue"
const isEdge = /Edg/.test(navigator.userAgent)
const { t } = useI18n()
const activeTab = ref<chrome.tabs.Tab>(emptyTab)
const manifest = reactive(chrome.runtime.getManifest())
const avaiable = ref(false)
const popularItems = reactive(config.data.popularSites)
const keyboard = reactive({
ctrl: false,
shift: false,
})
const horizontalScroller = ref<HTMLElement | null>(null)
......@@ -98,18 +99,24 @@ function handleKeydown(e: KeyboardEvent) {
}
if (e.code == "Backslash" && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
handleOpenSidebar()
openSidebar()
}
if (e.code == "ControlLeft" || e.code == "ControlRight") {
keyboard.ctrl = true
}
if (e.code == "ShiftLeft" || e.code == "ShiftRight") {
keyboard.shift = true
}
}
function hanldeKeyup(e: KeyboardEvent) {
if (e.code == "ControlLeft" || e.code == "ControlRight") {
keyboard.ctrl = false
}
if (e.code == "ShiftLeft" || e.code == "ShiftRight") {
keyboard.shift = false
}
}
const handleLocalChange = (changes: {
......@@ -125,7 +132,7 @@ async function handleWriteHtml() {
const tab = tabs[0]
if (tab) {
chrome.tabs.sendMessage(tab.id!, {
type: "pip",
type: MessageType.pip,
options: {
url: tab.url,
mode: "write-html",
......@@ -134,18 +141,31 @@ async function handleWriteHtml() {
}
}
async function handleOpenSidebar(url: string = "") {
const path = defaultSidebarPath + "?url=" + encodeURIComponent(url)
await chrome.sidePanel.setOptions({ path })
async function openSidebar(url = "") {
const win = await chrome.windows.getCurrent()
await chrome.storage.session.set({
sidebarUrls: { sidepanel: url },
})
await chrome.sidePanel.open({ windowId: win.id! })
window.close()
}
async function openContentSidebar(url = "") {
await chrome.storage.session.set({
sidebarUrls: { content: url },
})
await chrome.tabs.sendMessage(activeTab.value.id!, {
type: MessageType.openContentSidebar,
url,
})
window.close()
}
async function handleClickLaunch(url: string) {
console.log(activeTab.value)
await chrome.runtime.sendMessage({
type: "bg-pip-launch",
type: MessageType.bgPipLaunch,
url,
})
window.close()
......@@ -207,7 +227,7 @@ function showChatDocs() {
<div
:class="[
'flex gap-1 items-center text-sm my-2 px-2',
'flex gap-2 items-center text-sm my-2 px-2',
{
'text-rose-800': !avaiable,
},
......@@ -215,15 +235,15 @@ function showChatDocs() {
>
<div
v-if="avaiable"
class="w-4 h-4 rounded"
class="size-5"
:style="{
background:
'#8882 center / contain url(' + activeTab?.favIconUrl + ')',
background: 'center / contain url(' + activeTab?.favIconUrl + ')',
}"
></div>
<IconGppMaybe v-else class="w-4 h-4" />
<div v-if="avaiable" class="truncate">{{ host }}</div>
<div v-else class="truncate">{{ t("protectedTabTips") }}</div>
<IconGppMaybe v-else class="size-5" />
<div class="truncate">
{{ avaiable ? host : t("protectedTabTips") }}
</div>
</div>
<div
......@@ -234,10 +254,10 @@ function showChatDocs() {
>
<button
v-if="!pipWindow.tab"
:disabled="!avaiable"
class="hover:bg-background-mute bg-background-soft disabled:bg-background-soft disabled:opacity-65"
@click="handleWriteHtml"
:disabled="!avaiable"
:title="t('openInPip')"
@click="handleWriteHtml"
>
<IconAmpStories class="size-8 shrink-0" />
<div class="flex items-center justify-between flex-1 w-2/3 gap-2">
......@@ -251,14 +271,22 @@ function showChatDocs() {
<PipWindowActions v-else />
<button
class="hover:bg-background-mute bg-background-soft"
@click="() => handleOpenSidebar(activeTab.url)"
class="hover:bg-background-mute bg-background-soft disabled:bg-background-soft disabled:opacity-65"
:disabled="isEdge && !avaiable"
:title="t('openInSidebar')"
@click="
() => {
if (isEdge || keyboard.ctrl) {
return openContentSidebar()
}
openSidebar(activeTab.url)
}
"
>
<IconSplitscreenRight class="size-8 shrink-0 scale-95" />
<div class="flex items-center justify-between flex-1 w-2/3 gap-2">
<span class="text-sm font-bold leading-4 truncate">
{{ t("openInSidebar") }}
{{ isEdge ? t("openSidebar") : t("openInSidebar") }}
</span>
<span class="text-xs">CTRL + \</span>
</div>
......@@ -310,37 +338,25 @@ function showChatDocs() {
@wheel="(e) => horizontalScroller!.scrollLeft += e.deltaY"
@pointermove="(e) => e.buttons && (horizontalScroller!.scrollLeft -= e.movementX)"
>
<button
<SiteButton
v-for="item of popularItems"
class="group w-[58px] shrink-0 relative flex flex-col items-center rounded-lg px-2 py-3 bg-background-soft"
:icon="item.icon"
:title="item.title"
:badge="keyboard.shift || !avaiable ? 'popup' : 'sidebar'"
small
@click="
() =>
keyboard.ctrl
? handleOpenSidebar(item.url)
: handleClickLaunch(item.url)
() => {
if (keyboard.shift || !avaiable) {
return handleClickLaunch(item.url)
}
if (isEdge || keyboard.ctrl) {
return openContentSidebar(item.url)
}
openSidebar(item.url)
}
"
>
<div
class="size-6 rounded"
:style="{
background: 'center / contain url(' + item.icon + ')',
}"
></div>
<div
class="text-xs h-6 leading-3 line-clamp-2 flex flex-col justify-center"
>
<div>{{ item.title }}</div>
</div>
<IconAmpStories
v-if="!keyboard.ctrl"
class="size-3 absolute top-1 right-1 opacity-0 group-hover:opacity-65"
/>
<IconSplitscreenRight
v-else
class="size-3 absolute top-1 right-1 opacity-0 group-hover:opacity-65"
/>
</button>
/>
</div>
<div class="mt-3 flex items-center justify-center opacity-60">
......
<script setup lang="ts">
import { ref, onMounted, reactive, computed } from "vue"
import { getLocal, updateFrameNetRules } from "@/utils/ext"
import { ref, onMounted, reactive, computed, onUnmounted } from "vue"
import {
emptyTab,
getLocal,
getSession,
isProtectedUrl,
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"
import { MessageType } from "@/types"
import SiteButton from "@/components/SiteButton.vue"
const logoUrl = chrome.runtime.getURL("/logo.svg")
const { t } = useI18n()
const url = ref("")
const mode = ref("")
const popularItems = reactive(config.data.popularSites)
const recentItems = reactive<{ url: string; title: string; icon: string }[]>([])
const protectedUrl = computed(() => {
if (!url.value) {
return true
return isProtectedUrl(url.value)
})
const currentTab = reactive({
tabId: 0,
})
async function handleMessage(message: any) {
switch (message.type) {
case MessageType.openInSidebar:
if (!currentTab.tabId) {
const current = await chrome.tabs.getCurrent()
currentTab.tabId = current?.id || -1
}
if (message.tabId == currentTab.tabId) {
url.value = message.url
}
break
}
}
const u = new URL(url.value)
if (!["http:", "https:"].includes(u.protocol)) {
return true
async function updateRecentItems(pageInfo: {
url: string
title: string
icon: string
}) {
if (mode.value === "content") {
chrome.runtime.sendMessage({
type: MessageType.registerContentSidebar,
url: url.value,
})
}
return false
})
const { sidebarRecentItems } = await getLocal({
sidebarRecentItems: [] as {
url: string
icon: string
title: string
}[],
})
const index = sidebarRecentItems.findIndex((i) => i.url === pageInfo.url)
if (index !== -1) {
sidebarRecentItems.splice(index, 1)
}
sidebarRecentItems.unshift(pageInfo)
sidebarRecentItems.splice(12)
recentItems.splice(0, recentItems.length, ...sidebarRecentItems)
await chrome.storage.local.set({ sidebarRecentItems })
}
async function removeRecentItems(url: string) {
const { sidebarRecentItems } = await getLocal({
sidebarRecentItems: [] as {
url: string
icon: string
title: string
}[],
})
const index = sidebarRecentItems.findIndex((i) => i.url === url)
if (index !== -1) {
sidebarRecentItems.splice(index, 1)
}
recentItems.splice(0, recentItems.length, ...sidebarRecentItems)
await chrome.storage.local.set({ sidebarRecentItems })
}
onMounted(() => {
const q = new URLSearchParams(location.search)
const initUrl = q.get("url") || ""
url.value = initUrl
mode.value = q.get("mode") || "sidepanel"
getLocal({
popularSites: config.data.popularSites,
......@@ -43,6 +107,25 @@ onMounted(() => {
recentItems.splice(0, recentItems.length, ...sidebarRecentItems)
}
})
getSession({
sidebarUrls: {} as Record<string, string>,
}).then(({ sidebarUrls }) => {
console.log("[sidebar]", sidebarUrls, mode.value)
if (sidebarUrls && sidebarUrls[mode.value]) {
url.value = sidebarUrls[mode.value]
}
})
chrome.tabs.getCurrent().then((t) => {
currentTab.tabId = t?.id || -1
})
chrome.runtime.onMessage.addListener(handleMessage)
})
onUnmounted(() => {
chrome.runtime.onMessage.removeListener(handleMessage)
})
function go(link: string) {
......@@ -52,7 +135,7 @@ function go(link: string) {
<template>
<div class="w-full h-screen">
<Webview v-if="!protectedUrl" :url="url" />
<Webview v-if="!protectedUrl" :url="url" @page-info="updateRecentItems" />
<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">
......@@ -61,45 +144,25 @@ function go(link: string) {
</div>
<div class="grid grid-cols-4 gap-y-4 justify-between mt-24">
<button
<SiteButton
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"
:icon="item.icon"
:title="item.title"
badge="remove"
@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>
@remove="() => removeRecentItems(item.url)"
/>
</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
<SiteButton
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"
:icon="item.icon"
:title="item.title"
@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>
......
import "@/content/index"
import "@/pages/popup"
import "@/assets/main.css"
import { createApp } from "vue"
// import "@/content/index"
// import "@/pages/popup"
import Dev from "./Dev.vue"
import { i18n } from "@/utils/i18n"
const app = createApp(Dev)
app.use(i18n)
app.mount("#app")
......@@ -21,6 +21,12 @@ export const docsAddon = reactive({
active: false,
})
export const sidebarAddon = reactive({
visible: false,
hidden: false,
url: "",
})
type DocInputItem = {
key: string
kind: "file" | "string"
......
......@@ -4,10 +4,12 @@ declare namespace chrome.declarativeNetRequest {
}): Promise<Rule[]>
}
declare interface Manifest extends chrome.runtime.ManifestV3 {
declare interface ManifestPatch {
web_accessible_resources: Array<{
resources: string[]
matches: string[]
use_dynamic_url?: boolean
use_dynamic_url: boolean
}>
}
type Manifest = chrome.runtime.ManifestV3 & ManifestPatch
......@@ -5,7 +5,7 @@ export enum MessageType {
bgOpenPip = "bg-open-pip",
bgPipLaunch = "bg-pip-launch",
pipLaunch = "pip-launch",
contentMount = "content-mount",
contentMounted = "content-mount",
getPipWinInfo = "get-pip-win-info",
pipWinInfo = "pip-win-info",
updateWindow = "update-window",
......@@ -16,7 +16,10 @@ export enum MessageType {
invokeRequest = "invoke-request",
invokeResponse = "invoke-Response",
showChatDocs = "show-chat-docs",
frameReady = "frame-ready",
openInSidebar = "open-in-sidebar",
registerContentSidebar = "register-content-sidebar",
unregisterContentSidebar = "unregister-content-sidebar",
openContentSidebar = "open-content-sidebar",
}
export enum ServiceFunc {
......
......@@ -233,3 +233,9 @@ export function getPageIcon() {
return location.origin + "/favicon.ico"
}
export function autoPointerCapture(e: PointerEvent) {
if (e.buttons == 1) {
;(e.target as HTMLElement)?.setPointerCapture(e.pointerId)
}
}
......@@ -237,3 +237,28 @@ export async function updateFrameNetRules(
],
})
}
export function isProtectedUrl(url: string) {
try {
const u = new URL(url)
if (!["http:", "https:"].includes(u.protocol)) {
return true
}
const isEdge = /Edg/.test(navigator.userAgent)
if (isEdge && u.hostname == "microsoftedge.microsoft.com") {
return true
}
if (u.hostname == "chrome.google.com") {
return true
}
return false
} catch (e) {
console.warn(e)
}
return true
}
......@@ -51,7 +51,8 @@ export const semanticClip = (text: string, maxLength: number) => {
return text.slice(0, breakPoint)
}
export async function findFrameLoadUrl(urls: string[]) {
/** Find URL from the same origin that can be embedded */
export async function findFrameLoadUrl(urls: string[]): Promise<string> {
const abortController = new AbortController()
let resolve: null | ((url: string) => void) = null
......
......@@ -59,6 +59,7 @@ export default defineConfig({
outDir: "dist",
rollupOptions: {
input: {
// dev: "dev.html",
offscreen: "offscreen.html",
},
output: {
......
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