Skip to content

Commit 92536d9

Browse files
committed
feat: side panel refactor
1 parent 981aa52 commit 92536d9

112 files changed

Lines changed: 4099 additions & 1974 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ jobs:
2424
contents: write
2525
pull-requests: write
2626

27-
outputs:
28-
new_version: ${{ steps.get-version.outputs.version }}
29-
3027
steps:
3128
- uses: actions/checkout@v4
3229
with:
@@ -61,6 +58,4 @@ jobs:
6158
id: get-version
6259
run: |
6360
VERSION=$(node -p "require('./package.json').version")
64-
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
6561
echo "Released version: $VERSION" >> $GITHUB_STEP_SUMMARY
66-
echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV

entrypoints/content/components/ExternalImage.vue renamed to components/ExternalImage.vue

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ const imageCache = new LRUCache<string, string>({
2525
</script>
2626

2727
<script setup lang="ts">
28+
import { useLogger } from '@/composables/useLogger'
29+
import { fileToDataURL } from '@/utils/base64'
2830
import { c2bRpc } from '@/utils/rpc'
31+
import { forRuntimes } from '@/utils/runtime'
2932
import { classNames, ComponentClassAttr } from '@/utils/vue/utils'
3033
3134
const props = defineProps<{
@@ -35,27 +38,33 @@ const props = defineProps<{
3538
}>()
3639
3740
const dataUrl = ref('')
41+
const logger = useLogger()
3842
3943
const onError = () => {
4044
dataUrl.value = ''
4145
}
4246
4347
watchEffect(async () => {
44-
if (!props.src) {
48+
const src = props.src
49+
if (!src) {
4550
dataUrl.value = ''
4651
return
4752
}
48-
const cached = imageCache.get(props.src)
53+
const cached = imageCache.get(src)
4954
if (cached) {
5055
dataUrl.value = cached
5156
return
5257
}
53-
const r = await c2bRpc.fetchAsDataUrl(props.src)
54-
if ('dataUrl' in r) {
55-
dataUrl.value = r.dataUrl
58+
try {
59+
const r = await forRuntimes({
60+
content: () => c2bRpc.fetchAsDataUrl(src).then((r) => r.dataUrl),
61+
default: () => fetch(src).then((res) => res.blob()).then((blob) => fileToDataURL(blob)),
62+
})
63+
dataUrl.value = r
5664
}
57-
else {
58-
dataUrl.value = props.src
65+
catch (error) {
66+
logger.error('Failed to fetch image', error)
67+
onError()
5968
}
6069
})
6170
</script>

entrypoints/content/components/MarkdownViewer.test.ts renamed to components/MarkdownViewer.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { mount, VueWrapper } from '@vue/test-utils'
2-
import { beforeEach, describe, expect, it } from 'vitest'
2+
import { beforeAll, beforeEach, describe, expect, it } from 'vitest'
33
import { fakeBrowser } from 'wxt/testing'
44

5+
import { resetFakeEntrypoint } from '@/tests/utils/fake-browser'
56
import { sleep } from '@/utils/async'
67

7-
import { makeMarkdownIcon, makeParagraph, makeText } from '../utils/markdown/content'
8-
import MarkdownViewer from './MarkdownViewer.vue'
9-
108
async function waitForSelector<T extends VueWrapper, K extends (keyof HTMLElementTagNameMap)>(wrapper: T, selector: K, timeout?: number): Promise<VueWrapper<HTMLElementTagNameMap[K]>>
119
async function waitForSelector<T extends VueWrapper, K extends (keyof SVGElementTagNameMap)>(wrapper: T, selector: K, timeout?: number): Promise<VueWrapper<SVGElementTagNameMap[K]>>
1210
async function waitForSelector<T extends VueWrapper, K extends string>(wrapper: T, selector: K, timeout?: number): Promise<VueWrapper<Element>>
@@ -27,8 +25,13 @@ describe('MarkdownViewer', () => {
2725
// See https://webext-core.aklinker1.io/fake-browser/reseting-state
2826
fakeBrowser.reset()
2927
})
28+
beforeAll(() => {
29+
resetFakeEntrypoint()
30+
})
3031

3132
it('should render directive correctly (text)', async () => {
33+
const { makeText } = await import('@/entrypoints/sidepanel/utils/markdown/content')
34+
const MarkdownViewer = (await import('./MarkdownViewer.vue')).default
3235
const wrapper = mount(MarkdownViewer, {
3336
props: {
3437
text: `# Hello World ${makeText('BIG TEXT', { size: 50 })}`,
@@ -41,6 +44,8 @@ describe('MarkdownViewer', () => {
4144
})
4245

4346
it('should render directive correctly (icon)', async () => {
47+
const { makeMarkdownIcon } = await import('@/entrypoints/sidepanel/utils/markdown/content')
48+
const MarkdownViewer = (await import('./MarkdownViewer.vue')).default
4449
const wrapper = mount(MarkdownViewer, {
4550
props: {
4651
text: `Hello World ${makeMarkdownIcon('download')}`,
@@ -52,6 +57,8 @@ describe('MarkdownViewer', () => {
5257
})
5358

5459
it('should render directive correctly (paragraph)', async () => {
60+
const { makeParagraph } = await import('@/entrypoints/sidepanel/utils/markdown/content')
61+
const MarkdownViewer = (await import('./MarkdownViewer.vue')).default
5562
const wrapper = mount(MarkdownViewer, {
5663
props: {
5764
text: `Hello World

entrypoints/content/components/MarkdownViewer.vue renamed to components/MarkdownViewer.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ref, watchEffect } from 'vue'
1616
import IconMDLink from '@/assets/icons/md-link.svg?raw'
1717
import logger from '@/utils/logger'
1818
19-
import { getIconSvg, IconName, unescapeDirectiveText } from '../utils/markdown/content'
19+
import { getIconSvg, IconName, unescapeDirectiveText } from '../entrypoints/sidepanel/utils/markdown/content'
2020
2121
const log = logger.child('MarkdownViewer')
2222

components/ModelSelector.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@ import IconRedirect from '@/assets/icons/redirect.svg?component'
124124
import ModelLogo from '@/components/ModelLogo.vue'
125125
import { useOllamaStatusStore } from '@/entrypoints/content/store'
126126
import { deleteOllamaModel } from '@/entrypoints/content/utils/llm'
127-
import { showSettings } from '@/entrypoints/content/utils/settings'
128127
import { OLLAMA_SEARCH_URL } from '@/utils/constants'
129128
import { formatSize } from '@/utils/formatter'
130129
import { useI18n } from '@/utils/i18n'
131130
import { SUPPORTED_MODELS } from '@/utils/llm/web-llm'
131+
import { showSettings } from '@/utils/settings'
132132
import { getUserConfig } from '@/utils/user-config'
133133
import { classNames } from '@/utils/vue/utils'
134134
@@ -193,7 +193,7 @@ const onClickDelete = async (model: string) => {
193193
194194
const onClick = () => {
195195
if (modelList.value.length === 0) {
196-
showSettings(true, { scrollTarget: 'model-download-section' })
196+
showSettings({ scrollTarget: 'model-download-section' })
197197
}
198198
}
199199

entrypoints/content/components/RootProvider.vue renamed to components/RootProvider.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { useInjectContext } from '@/composables/useInjectContext'
77
88
const props = defineProps<{
9-
rootElement: HTMLDivElement
9+
rootElement: HTMLElement
1010
}>()
1111
1212
useInjectContext('selectorScrollListenElement').provide([props.rootElement])
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { onScopeDispose } from 'vue'
2+
3+
interface EventObject<L extends (...args: any[]) => void> {
4+
addListener: (listener: L) => void
5+
removeListener: (listener: L) => void
6+
}
7+
8+
// 提取事件对象的监听器类型
9+
type ExtractListener<T> = T extends EventObject<infer L> ? L : never
10+
11+
export function useExtensionEventListener<
12+
T extends EventObject<(...args: any[]) => void>,
13+
L extends ExtractListener<T> = ExtractListener<T>,
14+
>(
15+
eventObject: T,
16+
listener: L,
17+
) {
18+
eventObject.addListener(listener)
19+
onScopeDispose(() => {
20+
eventObject.removeListener(listener)
21+
})
22+
}

entrypoints/background/index.ts

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,87 +3,46 @@ import '@/utils/rpc'
33

44
import { browser } from 'wxt/browser'
55
import { defineBackground } from 'wxt/utils/define-background'
6-
import { storage } from 'wxt/utils/storage'
76

87
import { INVALID_URLS } from '@/utils/constants'
98
import { CONTEXT_MENU, CONTEXT_MENU_ITEM_TRANSLATE_PAGE, ContextMenuManager } from '@/utils/context-menu'
109
import { useGlobalI18n } from '@/utils/i18n'
1110
import logger from '@/utils/logger'
1211
import { bgBroadcastRpc } from '@/utils/rpc'
13-
import { isTabValid } from '@/utils/tab'
14-
import { getTabKeys } from '@/utils/tab-store'
12+
import { registerTabStoreCleanupListener } from '@/utils/tab-store'
1513
import { registerDeclarativeNetRequestRule } from '@/utils/web-request'
1614

1715
export default defineBackground(() => {
1816
if (import.meta.env.CHROME) {
1917
browser.storage.session.setAccessLevel({ accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS' })
2018
}
2119
registerDeclarativeNetRequestRule()
22-
23-
const tabsWaitingForOpen = new Set<number>()
20+
registerTabStoreCleanupListener()
2421

2522
browser.action.setTitle({ title: 'NativeMind' })
2623

27-
browser.action.onClicked.addListener(async (tab) => {
28-
logger.info('onClicked', tab)
29-
if (tab.id) {
30-
const validTab = await isTabValid(tab.id)
31-
if (validTab) {
32-
tabsWaitingForOpen.delete(tab.id)
33-
await bgBroadcastRpc.emit('toggleContainer', { _toTab: tab.id })
34-
}
35-
else {
36-
tabsWaitingForOpen.add(tab.id)
37-
}
38-
}
39-
})
40-
41-
const setPopupStatusBasedOnUrl = async (tabId: number, url: string) => {
42-
if (INVALID_URLS.some((regex) => regex.test(url))) {
43-
await browser.action.setPopup({ popup: 'popup.html' })
44-
}
45-
else {
46-
if (tabsWaitingForOpen.has(tabId)) {
47-
tabsWaitingForOpen.delete(tabId)
48-
await bgBroadcastRpc.emit('toggleContainer', { _toTab: tabId, open: true })
49-
}
50-
await browser.action.setPopup({ popup: '' })
51-
}
52-
}
24+
browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })
5325

54-
browser.tabs.onActivated.addListener(async ({ tabId }) => {
26+
browser.tabs.onActivated.addListener(async () => {
5527
const { t } = await useGlobalI18n()
5628
// reset the translate context menu to default
5729
const contextMenuManager = await ContextMenuManager.getInstance()
5830
contextMenuManager.updateContextMenu(CONTEXT_MENU_ITEM_TRANSLATE_PAGE.id, {
5931
title: t(CONTEXT_MENU_ITEM_TRANSLATE_PAGE.titleKey),
6032
contexts: CONTEXT_MENU_ITEM_TRANSLATE_PAGE.contexts,
6133
})
62-
const tab = await browser.tabs.get(tabId)
63-
const url = tab.url
64-
url && (await setPopupStatusBasedOnUrl(tabId, url))
6534
})
6635

6736
browser.tabs.onRemoved.addListener(async (tabId, removeInfo) => {
6837
logger.info('tab removed', { tabId, removeInfo, isFirefox: import.meta.env.FIREFOX })
69-
tabsWaitingForOpen.delete(tabId)
7038
bgBroadcastRpc.emit('tabRemoved', {
7139
tabId,
7240
...removeInfo,
7341
})
74-
if (import.meta.env.FIREFOX) {
75-
// Firefox does not support session storage in content scripts, so we need to clean up the tab store
76-
const keys = getTabKeys(tabId)
77-
logger.info('Cleaning up tab store for removed tab', { tabId, keys })
78-
await storage.removeItems(keys)
79-
}
8042
})
8143

8244
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
8345
logger.info('tab updated', { tabId, changeInfo, tab })
84-
if (tab.url) {
85-
await setPopupStatusBasedOnUrl(tabId, tab.url)
86-
}
8746

8847
bgBroadcastRpc.emit('tabUpdated', {
8948
tabId,

0 commit comments

Comments
 (0)