Commit 31d553e6 authored by Domi's avatar Domi

feat: content sidebar

parent 1d61640e
...@@ -4,11 +4,14 @@ ...@@ -4,11 +4,14 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Anything Copilot</title> <title>Anything Copilot DEV</title>
<base href="http://localhost:3000" />
</head> </head>
<body> <body>
<div id="app"></div> <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> <script type="module" src="./src/pages/dev.ts"></script>
</body> </body>
</html> </html>
...@@ -12,7 +12,6 @@ const ctx = await esbuild.context({ ...@@ -12,7 +12,6 @@ const ctx = await esbuild.context({
entryPoints: { entryPoints: {
// bg: "./src/bg/index.ts", // bg: "./src/bg/index.ts",
"js/content-main": "./src/content/main.ts", "js/content-main": "./src/content/main.ts",
"js/content-frame": "./src/content/frame.ts",
"js/pdf.worker": "./src/assets/pdf.worker.js", "js/pdf.worker": "./src/assets/pdf.worker.js",
}, },
bundle: true, bundle: true,
......
...@@ -98,9 +98,9 @@ ...@@ -98,9 +98,9 @@
"icon": "https://r2.ziziyi.com/copilot/gemini.svg" "icon": "https://r2.ziziyi.com/copilot/gemini.svg"
}, },
{ {
"url": "https://claude.ai/", "url": "https://runwayml.com/ai-tools/text-to-image/",
"title": "Claude", "title": "Text to Image",
"icon": "https://r2.ziziyi.com/copilot/claude-ai.svg" "icon": "https://r2.ziziyi.com/copilot/runway.png"
} }
] ]
} }
......
...@@ -6,6 +6,11 @@ import { ...@@ -6,6 +6,11 @@ import {
} from "@/types" } from "@/types"
import { waitMessage, tabUpdated, getLocal } from "@/utils/ext" import { waitMessage, tabUpdated, getLocal } from "@/utils/ext"
import { offscreen } from "./offscreen" import { offscreen } from "./offscreen"
import {
registerContentSidebar,
unregisterContentSidebar,
handleContentMounted,
} from "./sidebar"
import config from "@/assets/config.json" import config from "@/assets/config.json"
import { allFrameScript, mainContentScript } from "@/manifest" import { allFrameScript, mainContentScript } from "@/manifest"
...@@ -22,6 +27,11 @@ const contentScript = { ...@@ -22,6 +27,11 @@ const contentScript = {
} satisfies chrome.scripting.RegisteredContentScript } satisfies chrome.scripting.RegisteredContentScript
chrome.scripting.registerContentScripts([contentScript, mainContentScript]) 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) { async function openPipBackground(url: string) {
const tab = await chrome.tabs.create({ const tab = await chrome.tabs.create({
...@@ -43,7 +53,7 @@ async function pipLaunch(url: string) { ...@@ -43,7 +53,7 @@ async function pipLaunch(url: string) {
const tab = await chrome.tabs.create({ url }) const tab = await chrome.tabs.create({ url })
await waitMessage({ await waitMessage({
tabId: tab.id!, tabId: tab.id!,
type: MessageType.contentMount, type: MessageType.contentMounted,
}) })
chrome.tabs.sendMessage(tab.id!, { chrome.tabs.sendMessage(tab.id!, {
type: MessageType.pipLaunch, type: MessageType.pipLaunch,
...@@ -150,11 +160,18 @@ function handleMessage(message: any, sender: chrome.runtime.MessageSender) { ...@@ -150,11 +160,18 @@ function handleMessage(message: any, sender: chrome.runtime.MessageSender) {
case MessageType.invokeRequest: case MessageType.invokeRequest:
handleInvokeRequest(message, sender) handleInvokeRequest(message, sender)
break 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() { async function handleToggleMinimize() {
const { pipWindowId } = await chrome.storage.local.get({ pipWindowId: null }) const { pipWindowId } = await chrome.storage.local.get({ pipWindowId: null })
if (!pipWindowId) return if (!pipWindowId) return
...@@ -174,8 +191,6 @@ function handleCommand(command: string) { ...@@ -174,8 +191,6 @@ function handleCommand(command: string) {
} }
} }
chrome.commands.onCommand.addListener(handleCommand)
async function updateConfig() { async function updateConfig() {
const url = "https://config.ziziyi.com/anything-copilot" const url = "https://config.ziziyi.com/anything-copilot"
const now = Date.now() const now = Date.now()
...@@ -235,7 +250,3 @@ async function updateConfig() { ...@@ -235,7 +250,3 @@ async function updateConfig() {
loadCandidates, 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> <template>
<div></div> <div></div>
......
...@@ -10,6 +10,10 @@ const props = defineProps<{ ...@@ -10,6 +10,10 @@ const props = defineProps<{
url: string url: string
}>() }>()
const emit = defineEmits<{
pageInfo: [pageInfo: { url: string; title: string; icon: string }]
}>()
const frame = ref<HTMLIFrameElement>() const frame = ref<HTMLIFrameElement>()
const patchs = reactive(config.data.webviewPatchs) const patchs = reactive(config.data.webviewPatchs)
const loadUrls = reactive(config.data.loadCandidates) const loadUrls = reactive(config.data.loadCandidates)
...@@ -32,23 +36,7 @@ function handleFrameMessage(e: MessageEvent) { ...@@ -32,23 +36,7 @@ function handleFrameMessage(e: MessageEvent) {
pageInfo.title = e.data.title pageInfo.title = e.data.title
pageInfo.icon = e.data.icon pageInfo.icon = e.data.icon
getLocal({ emit("pageInfo", pageInfo)
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 break
} }
...@@ -118,7 +106,7 @@ watch(patch, async (patch) => { ...@@ -118,7 +106,7 @@ watch(patch, async (patch) => {
pageInfo.url = "" pageInfo.url = ""
pageInfo.title = "" pageInfo.title = ""
pageInfo.icon = "" pageInfo.icon = ""
const loadTimeout = 1000 * 1 const loadTimeout = 1000 * 100
try { try {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
......
...@@ -10,6 +10,7 @@ import { getDocItem, devConfig, type SiteConfig } from "./helper" ...@@ -10,6 +10,7 @@ import { getDocItem, devConfig, type SiteConfig } from "./helper"
import config from "@/assets/config.json" import config from "@/assets/config.json"
import { getLocal } from "@/utils/ext" import { getLocal } from "@/utils/ext"
import { chatDocPrompt } from "@/utils/prompt" import { chatDocPrompt } from "@/utils/prompt"
import { autoPointerCapture } from "@/utils/dom"
const { t } = useI18n() const { t } = useI18n()
const logoUrl = chrome.runtime.getURL("/logo.svg") const logoUrl = chrome.runtime.getURL("/logo.svg")
...@@ -188,8 +189,7 @@ onUnmounted(() => { ...@@ -188,8 +189,7 @@ onUnmounted(() => {
> >
<div <div
class="flex items-center px-4 pt-4 pb-1 select-none" 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" @pointermove="handlePointerMove"
@pointerup="adjustPosition" @pointerup="adjustPosition"
@pointercancel="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"> <script setup lang="ts">
import { pipLauncher } from "@/store" import { pipLauncher } from "@/store"
import PipLauncher from "@/components/PipLauncher.vue" import PipLauncher from "@/components/PipLauncher.vue"
import { MessageType } from "@/types"
import { useI18n } from "vue-i18n" import { useI18n } from "vue-i18n"
import ChatDocsAddon from "@/components/chatdocs/ChatDocsAddon.vue" import ChatDocsAddon from "@/components/chatdocs/ChatDocsAddon.vue"
import ContentSidebarAddon from "@/components/sidebar/ContentSidebarAddon.vue"
import { onMounted } from "vue"
const { t } = useI18n() const { t } = useI18n()
const topFrame = window.parent == window
onMounted(() => {
chrome.runtime?.sendMessage({ type: MessageType.contentMounted })
})
</script> </script>
<template> <template>
...@@ -15,4 +23,5 @@ const { t } = useI18n() ...@@ -15,4 +23,5 @@ const { t } = useI18n()
/> />
<ChatDocsAddon /> <ChatDocsAddon />
<ContentSidebarAddon v-if="topFrame" />
</template> </template>
import { MessageType } from "@/types"
chrome.runtime.sendMessage({
type: MessageType.frameReady,
})
import { mount, waitMountApp } from "./ui" 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 { ContentEventType, FrameMessageType, MessageType } from "@/types"
import Copilot from "./Copilot.vue" import Copilot from "./Copilot.vue"
import { waitMessage } from "@/utils/ext" import { waitMessage } from "@/utils/ext"
...@@ -46,6 +52,11 @@ function handleMessage( ...@@ -46,6 +52,11 @@ function handleMessage(
case MessageType.showChatDocs: case MessageType.showChatDocs:
chatDocsPanel.visible = true chatDocsPanel.visible = true
break break
case MessageType.openContentSidebar:
sidebarAddon.visible = true
sidebarAddon.hidden = false
sidebarAddon.url = message.url
break
} }
} }
...@@ -115,7 +126,7 @@ function run() { ...@@ -115,7 +126,7 @@ function run() {
async function postPageInfo() { async function postPageInfo() {
await new Promise((r) => setTimeout(r, 1000 * 3)) await new Promise((r) => setTimeout(r, 1000 * 3))
window.top?.postMessage( window.parent?.postMessage(
{ {
type: FrameMessageType.pageInfo, type: FrameMessageType.pageInfo,
url: location.href, url: location.href,
...@@ -126,10 +137,7 @@ async function postPageInfo() { ...@@ -126,10 +137,7 @@ async function postPageInfo() {
) )
} }
if (window.self == window.top) { function handleFrameMessage(e: MessageEvent) {
run()
} else {
window.addEventListener("message", (e) => {
if (!e.data || typeof e.data !== "object") return if (!e.data || typeof e.data !== "object") return
const type = e.data.type const type = e.data.type
switch (type) { switch (type) {
...@@ -143,10 +151,16 @@ if (window.self == window.top) { ...@@ -143,10 +151,16 @@ if (window.self == window.top) {
detail: { url: e.data.url }, detail: { url: e.data.url },
}) })
} }
}) }
window.top?.postMessage(
if (window.self == window.parent) {
run()
} else {
window.addEventListener("message", handleFrameMessage)
window.parent?.postMessage(
{ {
type: FrameMessageType.frameReady, type: FrameMessageType.frameReady,
url: location.href,
}, },
chrome.runtime.getURL("") chrome.runtime.getURL("")
) )
......
...@@ -33,7 +33,6 @@ export function mount(App: Component, doc = document) { ...@@ -33,7 +33,6 @@ export function mount(App: Component, doc = document) {
export function mountApp(doc = document) { export function mountApp(doc = document) {
mount(App, doc) mount(App, doc)
chrome.runtime?.sendMessage({ type: MessageType.contentMount })
} }
export function waitMountApp() { export function waitMountApp() {
......
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "እንደሚቆጠር መላክ", "autoSending": "እንደሚቆጠር መላክ",
"chooseContentRelevant": "ከመረጡ የሚያሳውቁ ተግባራዎችን ለመረጡ ይችላሉ", "chooseContentRelevant": "ከመረጡ የሚያሳውቁ ተግባራዎችን ለመረጡ ይችላሉ",
"notSupported": "ይህ ገጽ ራስ-ሰር መላክን አይደግፍም. እባክዎ መልዕክቱን ይቅዱ እና እራስዎ ይላኩ." "notSupported": "ይህ ገጽ ራስ-ሰር መላክን አይደግፍም. እባክዎ መልዕክቱን ይቅዱ እና እራስዎ ይላኩ."
} },
"openSidebar": "የጎን አሞሌውን ይክፈቱ"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "الإرسال التلقائي", "autoSending": "الإرسال التلقائي",
"chooseContentRelevant": "اختر محتوى أكثر صلة بالموضوع الذي ترغب في التعلم عنه", "chooseContentRelevant": "اختر محتوى أكثر صلة بالموضوع الذي ترغب في التعلم عنه",
"notSupported": "هذه الصفحة لا تدعم الإرسال التلقائي. يرجى نسخ الرسالة وإرسالها يدويًا." "notSupported": "هذه الصفحة لا تدعم الإرسال التلقائي. يرجى نسخ الرسالة وإرسالها يدويًا."
} },
"openSidebar": "افتح الشريط الجانبي"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Автоматично изпращане", "autoSending": "Автоматично изпращане",
"chooseContentRelevant": "Изберете съдържание, свързано с темата, за която искате да научите повече", "chooseContentRelevant": "Изберете съдържание, свързано с темата, за която искате да научите повече",
"notSupported": "Тази страница не поддържа автоматично изпращане. Моля, копирайте съобщението и го изпратете ръчно." "notSupported": "Тази страница не поддържа автоматично изпращане. Моля, копирайте съобщението и го изпратете ръчно."
} },
"openSidebar": "Отворете страничната лента"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "অটো প্রেরণ", "autoSending": "অটো প্রেরণ",
"chooseContentRelevant": "আপনি যে বিষয়ে আরও জানতে চান তা সম্পর্কিত কনটেন্ট চয়ন করুন", "chooseContentRelevant": "আপনি যে বিষয়ে আরও জানতে চান তা সম্পর্কিত কনটেন্ট চয়ন করুন",
"notSupported": "এই পৃষ্ঠাটি স্বয়ংক্রিয় প্রেরণকে সমর্থন করে না। দয়া করে বার্তাটি অনুলিপি করুন এবং এটি ম্যানুয়ালি প্রেরণ করুন।" "notSupported": "এই পৃষ্ঠাটি স্বয়ংক্রিয় প্রেরণকে সমর্থন করে না। দয়া করে বার্তাটি অনুলিপি করুন এবং এটি ম্যানুয়ালি প্রেরণ করুন।"
} },
"openSidebar": "সাইডবারটি খুলুন"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Enviament automàtic", "autoSending": "Enviament automàtic",
"chooseContentRelevant": "Trieu contingut més rellevant pel tema que voleu aprendre", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Automatické odesílání", "autoSending": "Automatické odesílání",
"chooseContentRelevant": "Vyberte obsah více relevantní k tématu, které chcete studovat", "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ě." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Auto afsendelse", "autoSending": "Auto afsendelse",
"chooseContentRelevant": "Vælg indhold mere relevant for det emne, du ønsker at lære om", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Automatisches Senden", "autoSending": "Automatisches Senden",
"chooseContentRelevant": "Wählen Sie Inhalte, die zum gewünschten Thema passen", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Αυτόματη αποστολή", "autoSending": "Αυτόματη αποστολή",
"chooseContentRelevant": "Επιλέξτε περιεχόμενο που σχετίζεται περισσότερο με το θέμα που θέλετε να μάθετε", "chooseContentRelevant": "Επιλέξτε περιεχόμενο που σχετίζεται περισσότερο με το θέμα που θέλετε να μάθετε",
"notSupported": "Αυτή η σελίδα δεν υποστηρίζει αυτόματη αποστολή. Αντιγράψτε το μήνυμα και στείλτε το χειροκίνητα." "notSupported": "Αυτή η σελίδα δεν υποστηρίζει αυτόματη αποστολή. Αντιγράψτε το μήνυμα και στείλτε το χειροκίνητα."
} },
"openSidebar": "Ανοίξτε την πλαϊνή μπάρα"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Auto Sending", "autoSending": "Auto Sending",
"chooseContentRelevant": "Choose content more relevant to the topic you want to learn about", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Envío Automático", "autoSending": "Envío Automático",
"chooseContentRelevant": "Elige contenido más relevante para el tema que deseas aprender", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Envío Automático", "autoSending": "Envío Automático",
"chooseContentRelevant": "Elige contenido más relevante para el tema que deseas aprender", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Automaatne Saatmine", "autoSending": "Automaatne Saatmine",
"chooseContentRelevant": "Valige teema kohta rohkem seotud sisu", "chooseContentRelevant": "Valige teema kohta rohkem seotud sisu",
"notSupported": "See leht ei toeta automaatset saatmist. Kopeerige sõnum ja saatke see käsitsi." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "ارسال خودکار", "autoSending": "ارسال خودکار",
"chooseContentRelevant": "محتوای مرتبط با موضوعی که می‌خواهید درباره آن یاد بگیرید را انتخاب کنید", "chooseContentRelevant": "محتوای مرتبط با موضوعی که می‌خواهید درباره آن یاد بگیرید را انتخاب کنید",
"notSupported": "این صفحه از ارسال خودکار پشتیبانی نمی کند. لطفا پیام را کپی کرده و به صورت دستی ارسال کنید." "notSupported": "این صفحه از ارسال خودکار پشتیبانی نمی کند. لطفا پیام را کپی کرده و به صورت دستی ارسال کنید."
} },
"openSidebar": "نوار کناری را باز کنید"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Automaattilähetys", "autoSending": "Automaattilähetys",
"chooseContentRelevant": "Valitse aiheeseesi liittyvämpi sisältö", "chooseContentRelevant": "Valitse aiheeseesi liittyvämpi sisältö",
"notSupported": "Tämä sivu ei tue automaattista lähettämistä. Kopioi viesti ja lähetä se manuaalisesti." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Auto Padala", "autoSending": "Auto Padala",
"chooseContentRelevant": "Pumili ng nilalaman na mas kaugnay sa paksa na nais mong malaman", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Envoi automatique", "autoSending": "Envoi automatique",
"chooseContentRelevant": "Choisissez un contenu plus pertinent pour le sujet que vous souhaitez apprendre", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "આપતી મોકલવું", "autoSending": "આપતી મોકલવું",
"chooseContentRelevant": "તમારા શીખવાના વિષય સાથે સંબંધિત કન્ટેન્ટ પસંદ કરો", "chooseContentRelevant": "તમારા શીખવાના વિષય સાથે સંબંધિત કન્ટેન્ટ પસંદ કરો",
"notSupported": "આ પૃષ્ઠ સ્વચાલિત મોકલવાનું સમર્થન કરતું નથી. કૃપા કરીને સંદેશની નકલ કરો અને તેને જાતે મોકલો." "notSupported": "આ પૃષ્ઠ સ્વચાલિત મોકલવાનું સમર્થન કરતું નથી. કૃપા કરીને સંદેશની નકલ કરો અને તેને જાતે મોકલો."
} },
"openSidebar": "સાઇડબાર ખોલો"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "שליחה אוטומטית", "autoSending": "שליחה אוטומטית",
"chooseContentRelevant": "בחר תוכן הקשור יותר לנושא שברצונך ללמוד עליו", "chooseContentRelevant": "בחר תוכן הקשור יותר לנושא שברצונך ללמוד עליו",
"notSupported": "דף זה אינו תומך בשליחה אוטומטית. אנא העתק את ההודעה ושלח אותה ידנית." "notSupported": "דף זה אינו תומך בשליחה אוטומטית. אנא העתק את ההודעה ושלח אותה ידנית."
} },
"openSidebar": "פתח את סרגל הצד"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "आत्म-भेजन", "autoSending": "आत्म-भेजन",
"chooseContentRelevant": "उस विषय के बारे में सीखना जिस पर आप चर्चा करना चाहते हैं, उससे संबंधित सामग्री चुनें", "chooseContentRelevant": "उस विषय के बारे में सीखना जिस पर आप चर्चा करना चाहते हैं, उससे संबंधित सामग्री चुनें",
"notSupported": "यह पृष्ठ स्वचालित भेजने का समर्थन नहीं करता है। कृपया संदेश कॉपी करें और इसे मैन्युअल रूप से भेजें।" "notSupported": "यह पृष्ठ स्वचालित भेजने का समर्थन नहीं करता है। कृपया संदेश कॉपी करें और इसे मैन्युअल रूप से भेजें।"
} },
"openSidebar": "साइडबार खोलें"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Automatsko slanje", "autoSending": "Automatsko slanje",
"chooseContentRelevant": "Odaberite sadržaj koji je relevantan za temu koju želite naučiti", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Automatikus küldés", "autoSending": "Automatikus küldés",
"chooseContentRelevant": "Válassza ki a témához relevánsabb tartalmat, amiről szeretne tanulni", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Pengiriman Otomatis", "autoSending": "Pengiriman Otomatis",
"chooseContentRelevant": "Pilih konten yang lebih relevan dengan topik yang ingin Anda pelajari", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Invio automatico", "autoSending": "Invio automatico",
"chooseContentRelevant": "Scegli contenuti più pertinenti all'argomento che vuoi apprendere", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "自動送信", "autoSending": "自動送信",
"chooseContentRelevant": "学びたいトピックに関連するコンテンツを選択してください", "chooseContentRelevant": "学びたいトピックに関連するコンテンツを選択してください",
"notSupported": "このページは、自動送信をサポートしていません。メッセージをコピーして手動で送信してください。" "notSupported": "このページは、自動送信をサポートしていません。メッセージをコピーして手動で送信してください。"
} },
"openSidebar": "サイドバーを開きます"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "ಸ್ವಯಂ ಕಳುಹಿಸುತ್ತಿದೆ", "autoSending": "ಸ್ವಯಂ ಕಳುಹಿಸುತ್ತಿದೆ",
"chooseContentRelevant": "ನೀವು ಕಲಿಯಬಯಸುವ ವಿಷಯಕ್ಕೆ ಹೆಚ್ಚಿನ ಸಂಬಂಧಪಟ್ಟ ವಿಷಯಗಳನ್ನು ಆರಿಸಿ", "chooseContentRelevant": "ನೀವು ಕಲಿಯಬಯಸುವ ವಿಷಯಕ್ಕೆ ಹೆಚ್ಚಿನ ಸಂಬಂಧಪಟ್ಟ ವಿಷಯಗಳನ್ನು ಆರಿಸಿ",
"notSupported": "ಈ ಪುಟವು ಸ್ವಯಂಚಾಲಿತ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಬೆಂಬಲಿಸುವುದಿಲ್ಲ. ದಯವಿಟ್ಟು ಸಂದೇಶವನ್ನು ನಕಲಿಸಿ ಮತ್ತು ಅದನ್ನು ಕೈಯಾರೆ ಕಳುಹಿಸಿ." "notSupported": "ಈ ಪುಟವು ಸ್ವಯಂಚಾಲಿತ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಬೆಂಬಲಿಸುವುದಿಲ್ಲ. ದಯವಿಟ್ಟು ಸಂದೇಶವನ್ನು ನಕಲಿಸಿ ಮತ್ತು ಅದನ್ನು ಕೈಯಾರೆ ಕಳುಹಿಸಿ."
} },
"openSidebar": "ಸೈಡ್‌ಬಾರ್ ತೆರೆಯಿರಿ"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "자동 전송", "autoSending": "자동 전송",
"chooseContentRelevant": "학습하고 싶은 주제와 관련된 콘텐츠를 선택하세요", "chooseContentRelevant": "학습하고 싶은 주제와 관련된 콘텐츠를 선택하세요",
"notSupported": "이 페이지는 자동 전송을 지원하지 않습니다. 메시지를 복사하여 수동으로 보내주십시오." "notSupported": "이 페이지는 자동 전송을 지원하지 않습니다. 메시지를 복사하여 수동으로 보내주십시오."
} },
"openSidebar": "사이드 바를 엽니 다"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Automatinis siuntimas", "autoSending": "Automatinis siuntimas",
"chooseContentRelevant": "Pasirinkite turinį, kuris yra labiau susijęs su jumis dominančia tema", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Automātiska sūtīšana", "autoSending": "Automātiska sūtīšana",
"chooseContentRelevant": "Izvēlieties saturu, kas ir saistīts ar tēmu, par kuru vēlaties uzzināt", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "ഓട്ടോ അയയ്ക്കൽ", "autoSending": "ഓട്ടോ അയയ്ക്കൽ",
"chooseContentRelevant": "നിങ്ങളുടെ അറിവിനായി കരുതോട്ട വിഷയത്തിനു കൂടുതൽ ബന്ധമായ ഉള്ളടക്കം തിരഞ്ഞെടുക്കുക", "chooseContentRelevant": "നിങ്ങളുടെ അറിവിനായി കരുതോട്ട വിഷയത്തിനു കൂടുതൽ ബന്ധമായ ഉള്ളടക്കം തിരഞ്ഞെടുക്കുക",
"notSupported": "ഈ പേജ് യാന്ത്രിക അയയ്ക്കുന്നതിനെ പിന്തുണയ്ക്കുന്നില്ല. സന്ദേശം പകർത്തി സ്വമേധയാ അയയ്ക്കുക." "notSupported": "ഈ പേജ് യാന്ത്രിക അയയ്ക്കുന്നതിനെ പിന്തുണയ്ക്കുന്നില്ല. സന്ദേശം പകർത്തി സ്വമേധയാ അയയ്ക്കുക."
} },
"openSidebar": "സൈഡ്ബാർ തുറക്കുക"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "स्वत: पाठवणे", "autoSending": "स्वत: पाठवणे",
"chooseContentRelevant": "तुम्हाला ओळखायचं विषयसंबंधित आशय निवडा", "chooseContentRelevant": "तुम्हाला ओळखायचं विषयसंबंधित आशय निवडा",
"notSupported": "हे पृष्ठ स्वयंचलित पाठविण्यास समर्थन देत नाही. कृपया संदेश कॉपी करा आणि तो व्यक्तिचलितपणे पाठवा." "notSupported": "हे पृष्ठ स्वयंचलित पाठविण्यास समर्थन देत नाही. कृपया संदेश कॉपी करा आणि तो व्यक्तिचलितपणे पाठवा."
} },
"openSidebar": "साइडबार उघडा"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Penghantaran Automatik", "autoSending": "Penghantaran Automatik",
"chooseContentRelevant": "Pilih kandungan yang lebih berkaitan dengan topik yang anda ingin ketahui", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Automatisch verzenden", "autoSending": "Automatisch verzenden",
"chooseContentRelevant": "Kies inhoud die relevanter is voor het onderwerp dat je wilt leren", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Auto Sending", "autoSending": "Auto Sending",
"chooseContentRelevant": "Velg innhold som er mer relevant for emnet du vil lære om", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Automatyczne Wysyłanie", "autoSending": "Automatyczne Wysyłanie",
"chooseContentRelevant": "Wybierz treść bardziej związana z tematem, który chcesz się dowiedzieć", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Envio Automático", "autoSending": "Envio Automático",
"chooseContentRelevant": "Escolha conteúdo mais relevante para o tópico que você deseja aprender", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Envio Automático", "autoSending": "Envio Automático",
"chooseContentRelevant": "Escolha conteúdo mais relevante para o tópico que deseja aprender", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Trimitere Automată", "autoSending": "Trimitere Automată",
"chooseContentRelevant": "Alegeți conținut mai relevant pentru subiectul pe care doriți să îl învățați", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Автоматическая Отправка", "autoSending": "Автоматическая Отправка",
"chooseContentRelevant": "Выберите более релевантный контент по теме, которую вы хотите изучить", "chooseContentRelevant": "Выберите более релевантный контент по теме, которую вы хотите изучить",
"notSupported": "Эта страница не поддерживает автоматическую отправку. Пожалуйста, скопируйте сообщение и отправьте его вручную." "notSupported": "Эта страница не поддерживает автоматическую отправку. Пожалуйста, скопируйте сообщение и отправьте его вручную."
} },
"openSidebar": "Откройте боковую панель"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Automatické Odosielanie", "autoSending": "Automatické Odosielanie",
"chooseContentRelevant": "Vyberte obsah, ktorý je viac relevantný pre tému, ktorú chcete študovať", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Avtomatsko Pošiljanje", "autoSending": "Avtomatsko Pošiljanje",
"chooseContentRelevant": "Izberite vsebino, ki je bolj relevantna za temo, ki se je želite naučiti", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Automatsko Slanje", "autoSending": "Automatsko Slanje",
"chooseContentRelevant": "Izaberite sadržaj koji je relevantniji za temu koju želite naučiti", "chooseContentRelevant": "Izaberite sadržaj koji je relevantniji za temu koju želite naučiti",
"notSupported": "Ова страница не подржава аутоматско слање. Копирајте поруку и пошаљите га ручно." "notSupported": "Ова страница не подржава аутоматско слање. Копирајте поруку и пошаљите га ручно."
} },
"openSidebar": "Отвори бочну траку"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Automatisk sändning", "autoSending": "Automatisk sändning",
"chooseContentRelevant": "Välj innehåll som är mer relevant för det ämne du vill lära dig om", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Kutuma Kiotomatiki", "autoSending": "Kutuma Kiotomatiki",
"chooseContentRelevant": "Chagua yaliyomo inayohusiana zaidi na mada unayotaka kujifunza kuhusu", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "தானாக அனுப்புதல்", "autoSending": "தானாக அனுப்புதல்",
"chooseContentRelevant": "நீங்கள் அறிந்திருக்க விரும்பும் பகுதிக்கு உரையாடல் தேர்ந்தெடுக்கவும்", "chooseContentRelevant": "நீங்கள் அறிந்திருக்க விரும்பும் பகுதிக்கு உரையாடல் தேர்ந்தெடுக்கவும்",
"notSupported": "இந்த பக்கம் தானியங்கி அனுப்புதலை ஆதரிக்காது. தயவுசெய்து செய்தியை நகலெடுத்து கைமுறையாக அனுப்புங்கள்." "notSupported": "இந்த பக்கம் தானியங்கி அனுப்புதலை ஆதரிக்காது. தயவுசெய்து செய்தியை நகலெடுத்து கைமுறையாக அனுப்புங்கள்."
} },
"openSidebar": "பக்கப்பட்டியைத் திறக்கவும்"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "స్వీయం పంపిణీ", "autoSending": "స్వీయం పంపిణీ",
"chooseContentRelevant": "మీరు కలిగిన విషయానికి అనుసంధానం కలిగిన కంటెంట్ ఎంచుకోండి", "chooseContentRelevant": "మీరు కలిగిన విషయానికి అనుసంధానం కలిగిన కంటెంట్ ఎంచుకోండి",
"notSupported": "ఈ పేజీ ఆటోమేటిక్ పంపడానికి మద్దతు ఇవ్వదు. దయచేసి సందేశాన్ని కాపీ చేసి మానవీయంగా పంపండి." "notSupported": "ఈ పేజీ ఆటోమేటిక్ పంపడానికి మద్దతు ఇవ్వదు. దయచేసి సందేశాన్ని కాపీ చేసి మానవీయంగా పంపండి."
} },
"openSidebar": "సైడ్‌బార్ తెరవండి"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "การส่งอัตโนมัติ", "autoSending": "การส่งอัตโนมัติ",
"chooseContentRelevant": "เลือกเนื้อหาที่เกี่ยวข้องมากขึ้นกับหัวข้อที่คุณต้องการเรียนรู้", "chooseContentRelevant": "เลือกเนื้อหาที่เกี่ยวข้องมากขึ้นกับหัวข้อที่คุณต้องการเรียนรู้",
"notSupported": "หน้านี้ไม่รองรับการส่งอัตโนมัติ กรุณาคัดลอกข้อความและส่งด้วยตนเอง" "notSupported": "หน้านี้ไม่รองรับการส่งอัตโนมัติ กรุณาคัดลอกข้อความและส่งด้วยตนเอง"
} },
"openSidebar": "เปิดแถบด้านข้าง"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Otomatik Gönderim", "autoSending": "Otomatik Gönderim",
"chooseContentRelevant": "Öğrenmek istediğiniz konuyla daha ilgili içerik seçin", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Автоматичне відправлення", "autoSending": "Автоматичне відправлення",
"chooseContentRelevant": "Виберіть вміст, який більше відповідає темі, яку ви хочете вивчити", "chooseContentRelevant": "Виберіть вміст, який більше відповідає темі, яку ви хочете вивчити",
"notSupported": "Ця сторінка не підтримує автоматичне надсилання. Будь ласка, скопіюйте повідомлення та надішліть його вручну." "notSupported": "Ця сторінка не підтримує автоматичне надсилання. Будь ласка, скопіюйте повідомлення та надішліть його вручну."
} },
"openSidebar": "Відкрийте бічну панель"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "Tự động Gửi", "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", "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." "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 @@ ...@@ -42,5 +42,6 @@
"autoSending": "自动发送", "autoSending": "自动发送",
"chooseContentRelevant": "选择与你想了解的主题更相关的内容", "chooseContentRelevant": "选择与你想了解的主题更相关的内容",
"notSupported": "此页面不支持自动发送,请复制消息发送" "notSupported": "此页面不支持自动发送,请复制消息发送"
} },
"openSidebar": "打开侧边栏"
} }
\ No newline at end of file
...@@ -42,5 +42,6 @@ ...@@ -42,5 +42,6 @@
"autoSending": "自動發送", "autoSending": "自動發送",
"chooseContentRelevant": "選擇與你想了解的主題更相關的內容", "chooseContentRelevant": "選擇與你想了解的主題更相關的內容",
"notSupported": "此頁面不支持自動發送。請複制消息並手動發送。" "notSupported": "此頁面不支持自動發送。請複制消息並手動發送。"
} },
"openSidebar": "打開側邊欄"
} }
\ No newline at end of file
...@@ -77,7 +77,6 @@ const manifest = { ...@@ -77,7 +77,6 @@ const manifest = {
"sidePanel", "sidePanel",
"declarativeNetRequestWithHostAccess", "declarativeNetRequestWithHostAccess",
"declarativeNetRequestFeedback", "declarativeNetRequestFeedback",
"webNavigation",
], ],
optional_permissions: [], optional_permissions: [],
host_permissions: ["<all_urls>"], host_permissions: ["<all_urls>"],
...@@ -93,11 +92,7 @@ const manifest = { ...@@ -93,11 +92,7 @@ const manifest = {
}, },
web_accessible_resources: [ web_accessible_resources: [
{ {
resources: ["logo.svg"], resources: ["/js/*", "/assets/*", "sidebar.html", "logo.svg"],
matches: ["<all_urls>"],
},
{
resources: ["/js/*", "/assets/*"],
matches: ["<all_urls>"], matches: ["<all_urls>"],
use_dynamic_url: true, use_dynamic_url: true,
}, },
...@@ -107,6 +102,6 @@ const manifest = { ...@@ -107,6 +102,6 @@ const manifest = {
? `script-src 'self' http://localhost:3000 'wasm-unsafe-eval';` ? `script-src 'self' http://localhost:3000 'wasm-unsafe-eval';`
: `script-src 'self' 'wasm-unsafe-eval'`, : `script-src 'self' 'wasm-unsafe-eval'`,
}, },
} satisfies Manifest as chrome.runtime.Manifest } satisfies Manifest as unknown as chrome.runtime.Manifest
export default 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" ...@@ -19,17 +19,18 @@ import IconAmpStories from "@/components/icons/IconAmpStories.vue"
import IconGppMaybe from "@/components/icons/IconGppMaybe.vue" import IconGppMaybe from "@/components/icons/IconGppMaybe.vue"
import IconHide from "@/components/icons/IconHide.vue" import IconHide from "@/components/icons/IconHide.vue"
import IconArrowCircleRight from "@/components/icons/IconArrowCircleRight.vue" import IconArrowCircleRight from "@/components/icons/IconArrowCircleRight.vue"
import IconClose from "@/components/icons/IconClose.vue"
import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue" import IconSplitscreenRight from "@/components/icons/IconSplitscreenRight.vue"
import SiteButton from "@/components/SiteButton.vue"
const isEdge = /Edg/.test(navigator.userAgent)
const { t } = useI18n() const { t } = useI18n()
const activeTab = ref<chrome.tabs.Tab>(emptyTab) const activeTab = ref<chrome.tabs.Tab>(emptyTab)
const manifest = reactive(chrome.runtime.getManifest()) const manifest = reactive(chrome.runtime.getManifest())
const avaiable = ref(false) const avaiable = ref(false)
const popularItems = reactive(config.data.popularSites) const popularItems = reactive(config.data.popularSites)
const keyboard = reactive({ const keyboard = reactive({
ctrl: false, ctrl: false,
shift: false,
}) })
const horizontalScroller = ref<HTMLElement | null>(null) const horizontalScroller = ref<HTMLElement | null>(null)
...@@ -98,18 +99,24 @@ function handleKeydown(e: KeyboardEvent) { ...@@ -98,18 +99,24 @@ function handleKeydown(e: KeyboardEvent) {
} }
if (e.code == "Backslash" && (e.ctrlKey || e.metaKey)) { if (e.code == "Backslash" && (e.ctrlKey || e.metaKey)) {
e.preventDefault() e.preventDefault()
handleOpenSidebar() openSidebar()
} }
if (e.code == "ControlLeft" || e.code == "ControlRight") { if (e.code == "ControlLeft" || e.code == "ControlRight") {
keyboard.ctrl = true keyboard.ctrl = true
} }
if (e.code == "ShiftLeft" || e.code == "ShiftRight") {
keyboard.shift = true
}
} }
function hanldeKeyup(e: KeyboardEvent) { function hanldeKeyup(e: KeyboardEvent) {
if (e.code == "ControlLeft" || e.code == "ControlRight") { if (e.code == "ControlLeft" || e.code == "ControlRight") {
keyboard.ctrl = false keyboard.ctrl = false
} }
if (e.code == "ShiftLeft" || e.code == "ShiftRight") {
keyboard.shift = false
}
} }
const handleLocalChange = (changes: { const handleLocalChange = (changes: {
...@@ -125,7 +132,7 @@ async function handleWriteHtml() { ...@@ -125,7 +132,7 @@ async function handleWriteHtml() {
const tab = tabs[0] const tab = tabs[0]
if (tab) { if (tab) {
chrome.tabs.sendMessage(tab.id!, { chrome.tabs.sendMessage(tab.id!, {
type: "pip", type: MessageType.pip,
options: { options: {
url: tab.url, url: tab.url,
mode: "write-html", mode: "write-html",
...@@ -134,18 +141,31 @@ async function handleWriteHtml() { ...@@ -134,18 +141,31 @@ async function handleWriteHtml() {
} }
} }
async function handleOpenSidebar(url: string = "") { async function openSidebar(url = "") {
const path = defaultSidebarPath + "?url=" + encodeURIComponent(url)
await chrome.sidePanel.setOptions({ path })
const win = await chrome.windows.getCurrent() const win = await chrome.windows.getCurrent()
await chrome.storage.session.set({
sidebarUrls: { sidepanel: url },
})
await chrome.sidePanel.open({ windowId: win.id! }) 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() window.close()
} }
async function handleClickLaunch(url: string) { async function handleClickLaunch(url: string) {
console.log(activeTab.value) console.log(activeTab.value)
await chrome.runtime.sendMessage({ await chrome.runtime.sendMessage({
type: "bg-pip-launch", type: MessageType.bgPipLaunch,
url, url,
}) })
window.close() window.close()
...@@ -207,7 +227,7 @@ function showChatDocs() { ...@@ -207,7 +227,7 @@ function showChatDocs() {
<div <div
:class="[ :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, 'text-rose-800': !avaiable,
}, },
...@@ -215,15 +235,15 @@ function showChatDocs() { ...@@ -215,15 +235,15 @@ function showChatDocs() {
> >
<div <div
v-if="avaiable" v-if="avaiable"
class="w-4 h-4 rounded" class="size-5"
:style="{ :style="{
background: background: 'center / contain url(' + activeTab?.favIconUrl + ')',
'#8882 center / contain url(' + activeTab?.favIconUrl + ')',
}" }"
></div> ></div>
<IconGppMaybe v-else class="w-4 h-4" /> <IconGppMaybe v-else class="size-5" />
<div v-if="avaiable" class="truncate">{{ host }}</div> <div class="truncate">
<div v-else class="truncate">{{ t("protectedTabTips") }}</div> {{ avaiable ? host : t("protectedTabTips") }}
</div>
</div> </div>
<div <div
...@@ -234,10 +254,10 @@ function showChatDocs() { ...@@ -234,10 +254,10 @@ function showChatDocs() {
> >
<button <button
v-if="!pipWindow.tab" v-if="!pipWindow.tab"
:disabled="!avaiable"
class="hover:bg-background-mute bg-background-soft disabled:bg-background-soft disabled:opacity-65" class="hover:bg-background-mute bg-background-soft disabled:bg-background-soft disabled:opacity-65"
@click="handleWriteHtml" :disabled="!avaiable"
:title="t('openInPip')" :title="t('openInPip')"
@click="handleWriteHtml"
> >
<IconAmpStories class="size-8 shrink-0" /> <IconAmpStories class="size-8 shrink-0" />
<div class="flex items-center justify-between flex-1 w-2/3 gap-2"> <div class="flex items-center justify-between flex-1 w-2/3 gap-2">
...@@ -251,14 +271,22 @@ function showChatDocs() { ...@@ -251,14 +271,22 @@ function showChatDocs() {
<PipWindowActions v-else /> <PipWindowActions v-else />
<button <button
class="hover:bg-background-mute bg-background-soft" class="hover:bg-background-mute bg-background-soft disabled:bg-background-soft disabled:opacity-65"
@click="() => handleOpenSidebar(activeTab.url)" :disabled="isEdge && !avaiable"
:title="t('openInSidebar')" :title="t('openInSidebar')"
@click="
() => {
if (isEdge || keyboard.ctrl) {
return openContentSidebar()
}
openSidebar(activeTab.url)
}
"
> >
<IconSplitscreenRight class="size-8 shrink-0 scale-95" /> <IconSplitscreenRight class="size-8 shrink-0 scale-95" />
<div class="flex items-center justify-between flex-1 w-2/3 gap-2"> <div class="flex items-center justify-between flex-1 w-2/3 gap-2">
<span class="text-sm font-bold leading-4 truncate"> <span class="text-sm font-bold leading-4 truncate">
{{ t("openInSidebar") }} {{ isEdge ? t("openSidebar") : t("openInSidebar") }}
</span> </span>
<span class="text-xs">CTRL + \</span> <span class="text-xs">CTRL + \</span>
</div> </div>
...@@ -310,37 +338,25 @@ function showChatDocs() { ...@@ -310,37 +338,25 @@ function showChatDocs() {
@wheel="(e) => horizontalScroller!.scrollLeft += e.deltaY" @wheel="(e) => horizontalScroller!.scrollLeft += e.deltaY"
@pointermove="(e) => e.buttons && (horizontalScroller!.scrollLeft -= e.movementX)" @pointermove="(e) => e.buttons && (horizontalScroller!.scrollLeft -= e.movementX)"
> >
<button <SiteButton
v-for="item of popularItems" 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=" @click="
() => () => {
keyboard.ctrl if (keyboard.shift || !avaiable) {
? handleOpenSidebar(item.url) return handleClickLaunch(item.url)
: handleClickLaunch(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 if (isEdge || keyboard.ctrl) {
v-if="!keyboard.ctrl" return openContentSidebar(item.url)
class="size-3 absolute top-1 right-1 opacity-0 group-hover:opacity-65" }
/> openSidebar(item.url)
<IconSplitscreenRight }
v-else "
class="size-3 absolute top-1 right-1 opacity-0 group-hover:opacity-65"
/> />
</button>
</div> </div>
<div class="mt-3 flex items-center justify-center opacity-60"> <div class="mt-3 flex items-center justify-center opacity-60">
......
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, reactive, computed } from "vue" import { ref, onMounted, reactive, computed, onUnmounted } from "vue"
import { getLocal, updateFrameNetRules } from "@/utils/ext" import {
emptyTab,
getLocal,
getSession,
isProtectedUrl,
updateFrameNetRules,
} from "@/utils/ext"
import config from "@/assets/config.json" import config from "@/assets/config.json"
import LoadingBar from "@/components/LoadingBar.vue" import LoadingBar from "@/components/LoadingBar.vue"
import Webview from "@/components/Webview.vue" import Webview from "@/components/Webview.vue"
import { useI18n } from "@/utils/i18n" import { useI18n } from "@/utils/i18n"
import { MessageType } from "@/types"
import SiteButton from "@/components/SiteButton.vue"
const logoUrl = chrome.runtime.getURL("/logo.svg") const logoUrl = chrome.runtime.getURL("/logo.svg")
const { t } = useI18n() const { t } = useI18n()
const url = ref("") const url = ref("")
const mode = ref("")
const popularItems = reactive(config.data.popularSites) const popularItems = reactive(config.data.popularSites)
const recentItems = reactive<{ url: string; title: string; icon: string }[]>([]) const recentItems = reactive<{ url: string; title: string; icon: string }[]>([])
const protectedUrl = computed(() => { const protectedUrl = computed(() => {
if (!url.value) { return isProtectedUrl(url.value)
return true })
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
}
}
async function updateRecentItems(pageInfo: {
url: string
title: string
icon: string
}) {
if (mode.value === "content") {
chrome.runtime.sendMessage({
type: MessageType.registerContentSidebar,
url: url.value,
})
} }
const u = new URL(url.value) const { sidebarRecentItems } = await getLocal({
if (!["http:", "https:"].includes(u.protocol)) { sidebarRecentItems: [] as {
return true 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)
return false 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(() => { onMounted(() => {
const q = new URLSearchParams(location.search) const q = new URLSearchParams(location.search)
const initUrl = q.get("url") || "" mode.value = q.get("mode") || "sidepanel"
url.value = initUrl
getLocal({ getLocal({
popularSites: config.data.popularSites, popularSites: config.data.popularSites,
...@@ -43,6 +107,25 @@ onMounted(() => { ...@@ -43,6 +107,25 @@ onMounted(() => {
recentItems.splice(0, recentItems.length, ...sidebarRecentItems) 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) { function go(link: string) {
...@@ -52,7 +135,7 @@ function go(link: string) { ...@@ -52,7 +135,7 @@ function go(link: string) {
<template> <template>
<div class="w-full h-screen"> <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 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"> <div class="flex flex-col items-center gap-2 mx-auto mt-16">
...@@ -61,45 +144,25 @@ function go(link: string) { ...@@ -61,45 +144,25 @@ function go(link: string) {
</div> </div>
<div class="grid grid-cols-4 gap-y-4 justify-between mt-24"> <div class="grid grid-cols-4 gap-y-4 justify-between mt-24">
<button <SiteButton
v-for="item of recentItems" 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)" @click="go(item.url)"
> @remove="() => removeRecentItems(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 class="text-center my-3">Popular</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="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"> <div class="grid grid-cols-4 gap-y-4 justify-between">
<button <SiteButton
v-for="item of popularItems" 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)" @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> </div>
</div> </div>
......
import "@/content/index" import "@/assets/main.css"
import "@/pages/popup" 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({ ...@@ -21,6 +21,12 @@ export const docsAddon = reactive({
active: false, active: false,
}) })
export const sidebarAddon = reactive({
visible: false,
hidden: false,
url: "",
})
type DocInputItem = { type DocInputItem = {
key: string key: string
kind: "file" | "string" kind: "file" | "string"
......
...@@ -4,10 +4,12 @@ declare namespace chrome.declarativeNetRequest { ...@@ -4,10 +4,12 @@ declare namespace chrome.declarativeNetRequest {
}): Promise<Rule[]> }): Promise<Rule[]>
} }
declare interface Manifest extends chrome.runtime.ManifestV3 { declare interface ManifestPatch {
web_accessible_resources: Array<{ web_accessible_resources: Array<{
resources: string[] resources: string[]
matches: string[] matches: string[]
use_dynamic_url?: boolean use_dynamic_url: boolean
}> }>
} }
type Manifest = chrome.runtime.ManifestV3 & ManifestPatch
...@@ -5,7 +5,7 @@ export enum MessageType { ...@@ -5,7 +5,7 @@ export enum MessageType {
bgOpenPip = "bg-open-pip", bgOpenPip = "bg-open-pip",
bgPipLaunch = "bg-pip-launch", bgPipLaunch = "bg-pip-launch",
pipLaunch = "pip-launch", pipLaunch = "pip-launch",
contentMount = "content-mount", contentMounted = "content-mount",
getPipWinInfo = "get-pip-win-info", getPipWinInfo = "get-pip-win-info",
pipWinInfo = "pip-win-info", pipWinInfo = "pip-win-info",
updateWindow = "update-window", updateWindow = "update-window",
...@@ -16,7 +16,10 @@ export enum MessageType { ...@@ -16,7 +16,10 @@ export enum MessageType {
invokeRequest = "invoke-request", invokeRequest = "invoke-request",
invokeResponse = "invoke-Response", invokeResponse = "invoke-Response",
showChatDocs = "show-chat-docs", showChatDocs = "show-chat-docs",
frameReady = "frame-ready", openInSidebar = "open-in-sidebar",
registerContentSidebar = "register-content-sidebar",
unregisterContentSidebar = "unregister-content-sidebar",
openContentSidebar = "open-content-sidebar",
} }
export enum ServiceFunc { export enum ServiceFunc {
......
...@@ -233,3 +233,9 @@ export function getPageIcon() { ...@@ -233,3 +233,9 @@ export function getPageIcon() {
return location.origin + "/favicon.ico" 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( ...@@ -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) => { ...@@ -51,7 +51,8 @@ export const semanticClip = (text: string, maxLength: number) => {
return text.slice(0, breakPoint) 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() const abortController = new AbortController()
let resolve: null | ((url: string) => void) = null let resolve: null | ((url: string) => void) = null
......
...@@ -59,6 +59,7 @@ export default defineConfig({ ...@@ -59,6 +59,7 @@ export default defineConfig({
outDir: "dist", outDir: "dist",
rollupOptions: { rollupOptions: {
input: { input: {
// dev: "dev.html",
offscreen: "offscreen.html", offscreen: "offscreen.html",
}, },
output: { 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