镜像自地址
https://github.com/lhly/search-syntax-pro.git
已同步 2026-10-20 14:27:47 +08:00
feat(窗口管理): 添加独立窗口功能并实现国际化支持
添加 detached 窗口模式,允许用户将 popup 内容弹出到独立窗口 实现窗口位置记忆和单例模式管理 更新国际化配置和界面标题 添加弹出按钮组件和窗口管理服务
这个提交包含在:
父节点
4c728be987
当前提交
5bd22ec579
@ -98,5 +98,17 @@
|
|||||||
"clear_all_data": {
|
"clear_all_data": {
|
||||||
"message": "Clear All Data",
|
"message": "Clear All Data",
|
||||||
"description": "Clear all data button"
|
"description": "Clear all data button"
|
||||||
|
},
|
||||||
|
"popup_title": {
|
||||||
|
"message": "SearchSyntax Pro",
|
||||||
|
"description": "Popup window title"
|
||||||
|
},
|
||||||
|
"detached_title": {
|
||||||
|
"message": "SearchSyntax Pro - Detached Window",
|
||||||
|
"description": "Detached window title"
|
||||||
|
},
|
||||||
|
"options_title": {
|
||||||
|
"message": "SearchSyntax Pro - Settings",
|
||||||
|
"description": "Options page title"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"app_name": {
|
"app_name": {
|
||||||
"message": "搜索语法大师",
|
"message": "SearchSyntax Pro",
|
||||||
"description": "搜索语法大师 - SearchSyntax Pro"
|
"description": "SearchSyntax Pro - Advanced Search Syntax Tool"
|
||||||
},
|
},
|
||||||
"app_description": {
|
"app_description": {
|
||||||
"message": "搜索语法可视化工具,降低高级搜索语法使用门槛",
|
"message": "搜索语法可视化工具,降低高级搜索语法使用门槛",
|
||||||
@ -98,5 +98,17 @@
|
|||||||
"clear_all_data": {
|
"clear_all_data": {
|
||||||
"message": "清除所有数据",
|
"message": "清除所有数据",
|
||||||
"description": "清除所有数据按钮"
|
"description": "清除所有数据按钮"
|
||||||
|
},
|
||||||
|
"popup_title": {
|
||||||
|
"message": "SearchSyntax Pro",
|
||||||
|
"description": "Popup window title"
|
||||||
|
},
|
||||||
|
"detached_title": {
|
||||||
|
"message": "SearchSyntax Pro - 独立窗口",
|
||||||
|
"description": "Detached window title"
|
||||||
|
},
|
||||||
|
"options_title": {
|
||||||
|
"message": "SearchSyntax Pro - 设置",
|
||||||
|
"description": "Options page title"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,7 +66,7 @@
|
|||||||
|
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": ["icons/*"],
|
"resources": ["icons/*", "src/detached/index.html"],
|
||||||
"matches": ["<all_urls>"]
|
"matches": ["<all_urls>"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
93
src/components/PopoutButton.tsx
普通文件
93
src/components/PopoutButton.tsx
普通文件
@ -0,0 +1,93 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { WindowManager } from '@/services/window-manager'
|
||||||
|
import { useTranslation } from '@/i18n'
|
||||||
|
|
||||||
|
interface PopoutButtonProps {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Popout Button Component
|
||||||
|
*
|
||||||
|
* 在 popup 窗口中显示的"弹出到新窗口"按钮
|
||||||
|
* 点击后将当前 popup 内容在独立窗口中打开
|
||||||
|
*/
|
||||||
|
export function PopoutButton({ className = '' }: PopoutButtonProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
// 只在 popup 模式下显示此按钮
|
||||||
|
if (WindowManager.isDetachedWindow()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePopout = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
await WindowManager.popoutToDetachedWindow()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('弹出窗口失败:', error)
|
||||||
|
setIsLoading(false)
|
||||||
|
alert('打开独立窗口失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handlePopout}
|
||||||
|
disabled={isLoading}
|
||||||
|
className={`
|
||||||
|
popout-button
|
||||||
|
flex items-center justify-center
|
||||||
|
w-8 h-8
|
||||||
|
rounded-md
|
||||||
|
text-white hover:bg-white/20
|
||||||
|
transition-colors duration-200
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
|
${className}
|
||||||
|
`}
|
||||||
|
title={t('popup.popoutButtonTooltip')}
|
||||||
|
aria-label={t('popup.popoutButtonTitle')}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
// 加载动画
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
// 弹出窗口图标 (使用 SVG)
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
src/detached/index.html
普通文件
18
src/detached/index.html
普通文件
@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN" class="chrome-extension-detached-root">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>SearchSyntax Pro - Detached Window</title>
|
||||||
|
<link rel="stylesheet" href="../../src/styles/globals.css">
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="chrome-extension-detached-body">
|
||||||
|
<div id="app" class="chrome-extension-detached">
|
||||||
|
<!-- React应用将在这里渲染 -->
|
||||||
|
</div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
src/detached/main.tsx
普通文件
24
src/detached/main.tsx
普通文件
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from '../popup/App'
|
||||||
|
import '../styles/globals.css'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detached Window Entry Point
|
||||||
|
*
|
||||||
|
* 这是独立窗口的入口文件,完全复用 popup/App.tsx 组件
|
||||||
|
* 通过 window 对象标记当前运行环境为 detached 模式
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 标记当前窗口为 detached 模式,供组件内部判断使用
|
||||||
|
;(window as any).__SSP_WINDOW_MODE__ = 'detached'
|
||||||
|
|
||||||
|
// 设置国际化标题
|
||||||
|
document.title = chrome.i18n.getMessage('detached_title')
|
||||||
|
|
||||||
|
// 渲染应用
|
||||||
|
ReactDOM.createRoot(document.getElementById('app')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
@ -35,9 +35,11 @@ const translations: Record<Language, Record<string, string>> = {
|
|||||||
'copyButton.tooltipCopy': '复制',
|
'copyButton.tooltipCopy': '复制',
|
||||||
'copyButton.tooltipCopied': '已复制!',
|
'copyButton.tooltipCopied': '已复制!',
|
||||||
'copyButton.copyError': '复制失败',
|
'copyButton.copyError': '复制失败',
|
||||||
'popup.headerTitle': '搜索语法大师',
|
'popup.headerTitle': 'SearchSyntax Pro',
|
||||||
'popup.footer.currentEngine': '当前引擎: {engine}',
|
'popup.footer.currentEngine': '当前引擎: {engine}',
|
||||||
'popup.settingsButtonTitle': '设置',
|
'popup.settingsButtonTitle': '设置',
|
||||||
|
'popup.popoutButtonTitle': '弹出到新窗口',
|
||||||
|
'popup.popoutButtonTooltip': '在独立窗口中使用,不会因浏览器操作而关闭',
|
||||||
'searchForm.keywordLabel': '搜索关键词',
|
'searchForm.keywordLabel': '搜索关键词',
|
||||||
'searchForm.keywordPlaceholder': '输入要搜索的关键词...',
|
'searchForm.keywordPlaceholder': '输入要搜索的关键词...',
|
||||||
'searchForm.engineLabel': '搜索引擎',
|
'searchForm.engineLabel': '搜索引擎',
|
||||||
@ -70,7 +72,7 @@ const translations: Record<Language, Record<string, string>> = {
|
|||||||
'searchHistory.viewAllHint': '或前往设置页面查看完整历史记录',
|
'searchHistory.viewAllHint': '或前往设置页面查看完整历史记录',
|
||||||
'searchHistory.viewAllInSettings': '查看全部 {total} 条记录,请前往设置页面',
|
'searchHistory.viewAllInSettings': '查看全部 {total} 条记录,请前往设置页面',
|
||||||
'searchHistory.goToSettings': '打开设置页面',
|
'searchHistory.goToSettings': '打开设置页面',
|
||||||
'options.header.title': '搜索语法大师 - 设置',
|
'options.header.title': 'SearchSyntax Pro - 设置',
|
||||||
'options.header.subtitle': '设置和配置',
|
'options.header.subtitle': '设置和配置',
|
||||||
'options.tabs.settings': '设置',
|
'options.tabs.settings': '设置',
|
||||||
'options.tabs.shortcuts': '快捷键',
|
'options.tabs.shortcuts': '快捷键',
|
||||||
@ -406,6 +408,8 @@ const translations: Record<Language, Record<string, string>> = {
|
|||||||
'popup.headerTitle': 'SearchSyntax Pro',
|
'popup.headerTitle': 'SearchSyntax Pro',
|
||||||
'popup.footer.currentEngine': 'Current engine: {engine}',
|
'popup.footer.currentEngine': 'Current engine: {engine}',
|
||||||
'popup.settingsButtonTitle': 'Settings',
|
'popup.settingsButtonTitle': 'Settings',
|
||||||
|
'popup.popoutButtonTitle': 'Pop out to new window',
|
||||||
|
'popup.popoutButtonTooltip': 'Use in a standalone window that won\'t close with browser actions',
|
||||||
'searchForm.keywordLabel': 'Search keywords',
|
'searchForm.keywordLabel': 'Search keywords',
|
||||||
'searchForm.keywordPlaceholder': 'Enter the keywords you want to search...',
|
'searchForm.keywordPlaceholder': 'Enter the keywords you want to search...',
|
||||||
'searchForm.engineLabel': 'Search engine',
|
'searchForm.engineLabel': 'Search engine',
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>SSP智能搜索插件 - 设置</title>
|
<title>SearchSyntax Pro - Settings</title>
|
||||||
<link rel="stylesheet" href="../../src/styles/globals.css">
|
<link rel="stylesheet" href="../../src/styles/globals.css">
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import ReactDOM from 'react-dom/client'
|
|||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import '../styles/globals.css'
|
import '../styles/globals.css'
|
||||||
|
|
||||||
|
// 设置国际化标题
|
||||||
|
document.title = chrome.i18n.getMessage('options_title')
|
||||||
|
|
||||||
// 初始化React应用
|
// 初始化React应用
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('app') as HTMLElement
|
document.getElementById('app') as HTMLElement
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { SearchForm } from '@/components/SearchForm'
|
|||||||
import { QueryPreview } from '@/components/QueryPreview'
|
import { QueryPreview } from '@/components/QueryPreview'
|
||||||
import { SearchHistory as SearchHistoryComponent } from '@/components/SearchHistory'
|
import { SearchHistory as SearchHistoryComponent } from '@/components/SearchHistory'
|
||||||
import { SettingsButton } from '@/components/SettingsButton'
|
import { SettingsButton } from '@/components/SettingsButton'
|
||||||
|
import { PopoutButton } from '@/components/PopoutButton'
|
||||||
import { LogoIcon } from '@/components/Logo'
|
import { LogoIcon } from '@/components/Logo'
|
||||||
import { TemplateSelector } from '@/components/TemplateSelector'
|
import { TemplateSelector } from '@/components/TemplateSelector'
|
||||||
import { SuggestionPanel } from '@/components/SuggestionPanel'
|
import { SuggestionPanel } from '@/components/SuggestionPanel'
|
||||||
@ -13,6 +14,7 @@ import { SearchAdapterFactory } from '@/services/adapters'
|
|||||||
import { EnginePreferenceService } from '@/services/engine-preference'
|
import { EnginePreferenceService } from '@/services/engine-preference'
|
||||||
import { templateManager } from '@/services/template-manager'
|
import { templateManager } from '@/services/template-manager'
|
||||||
import { shortcutManager } from '@/services/shortcut-manager'
|
import { shortcutManager } from '@/services/shortcut-manager'
|
||||||
|
import { WindowManager } from '@/services/window-manager'
|
||||||
import { TranslationProvider, useTranslation } from '@/i18n'
|
import { TranslationProvider, useTranslation } from '@/i18n'
|
||||||
import { useExtensionVersion } from '@/utils/version'
|
import { useExtensionVersion } from '@/utils/version'
|
||||||
import { autoMigrateStorage } from '@/utils/migration'
|
import { autoMigrateStorage } from '@/utils/migration'
|
||||||
@ -304,6 +306,11 @@ function App() {
|
|||||||
try {
|
try {
|
||||||
await templateManager.initialize()
|
await templateManager.initialize()
|
||||||
await shortcutManager.initialize('popup')
|
await shortcutManager.initialize('popup')
|
||||||
|
|
||||||
|
// v1.6.2: 如果是 detached 窗口,启用窗口位置追踪
|
||||||
|
if (WindowManager.isDetachedWindow()) {
|
||||||
|
WindowManager.setupWindowPositionTracking()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('初始化服务失败:', error)
|
console.error('初始化服务失败:', error)
|
||||||
}
|
}
|
||||||
@ -480,6 +487,7 @@ function PopupContent({
|
|||||||
<h1 className="text-lg font-semibold">{t('popup.headerTitle')}</h1>
|
<h1 className="text-lg font-semibold">{t('popup.headerTitle')}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
<PopoutButton />
|
||||||
<ShortcutHintTrigger onClick={() => setShowShortcutHint(true)} />
|
<ShortcutHintTrigger onClick={() => setShowShortcutHint(true)} />
|
||||||
<SettingsButton />
|
<SettingsButton />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>SSP智能搜索插件</title>
|
<title>SearchSyntax Pro</title>
|
||||||
<link rel="stylesheet" href="../../src/styles/globals.css">
|
<link rel="stylesheet" href="../../src/styles/globals.css">
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import ReactDOM from 'react-dom/client'
|
|||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import '../styles/globals.css'
|
import '../styles/globals.css'
|
||||||
|
|
||||||
|
// 设置国际化标题
|
||||||
|
document.title = chrome.i18n.getMessage('popup_title')
|
||||||
|
|
||||||
// 初始化React应用
|
// 初始化React应用
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('app') as HTMLElement
|
document.getElementById('app') as HTMLElement
|
||||||
|
|||||||
242
src/services/window-manager.ts
普通文件
242
src/services/window-manager.ts
普通文件
@ -0,0 +1,242 @@
|
|||||||
|
/**
|
||||||
|
* Window Manager Service
|
||||||
|
*
|
||||||
|
* 管理 popup 和 detached 窗口的创建、切换和状态同步
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface WindowPosition {
|
||||||
|
left?: number
|
||||||
|
top?: number
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DetachedWindowConfig {
|
||||||
|
url: string
|
||||||
|
position?: WindowPosition
|
||||||
|
focused?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY_WINDOW_POSITION = 'detached_window_position'
|
||||||
|
const STORAGE_KEY_WINDOW_ID = 'detached_window_id'
|
||||||
|
const DEFAULT_WINDOW_SIZE = { width: 420, height: 700 }
|
||||||
|
|
||||||
|
export class WindowManager {
|
||||||
|
/**
|
||||||
|
* 获取当前窗口模式
|
||||||
|
*/
|
||||||
|
static getWindowMode(): 'popup' | 'detached' {
|
||||||
|
return (window as any).__SSP_WINDOW_MODE__ === 'detached' ? 'detached' : 'popup'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为 detached 窗口
|
||||||
|
*/
|
||||||
|
static isDetachedWindow(): boolean {
|
||||||
|
return this.getWindowMode() === 'detached'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取保存的窗口位置
|
||||||
|
*/
|
||||||
|
static async getSavedWindowPosition(): Promise<WindowPosition> {
|
||||||
|
try {
|
||||||
|
const result = await chrome.storage.local.get(STORAGE_KEY_WINDOW_POSITION)
|
||||||
|
return result[STORAGE_KEY_WINDOW_POSITION] || {}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取窗口位置失败:', error)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存窗口位置
|
||||||
|
*/
|
||||||
|
static async saveWindowPosition(position: WindowPosition): Promise<void> {
|
||||||
|
try {
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
[STORAGE_KEY_WINDOW_POSITION]: position
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存窗口位置失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前保存的 detached 窗口 ID
|
||||||
|
*/
|
||||||
|
static async getDetachedWindowId(): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
const result = await chrome.storage.local.get(STORAGE_KEY_WINDOW_ID)
|
||||||
|
return result[STORAGE_KEY_WINDOW_ID] || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取窗口ID失败:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 detached 窗口 ID
|
||||||
|
*/
|
||||||
|
static async saveDetachedWindowId(windowId: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
[STORAGE_KEY_WINDOW_ID]: windowId
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存窗口ID失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除保存的 detached 窗口 ID
|
||||||
|
*/
|
||||||
|
static async clearDetachedWindowId(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await chrome.storage.local.remove(STORAGE_KEY_WINDOW_ID)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除窗口ID失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查指定窗口是否存在
|
||||||
|
*/
|
||||||
|
static async isWindowExists(windowId: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const window = await chrome.windows.get(windowId)
|
||||||
|
return window !== null
|
||||||
|
} catch (error) {
|
||||||
|
// 窗口不存在会抛出异常
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 detached 窗口(带单例模式检查)
|
||||||
|
*/
|
||||||
|
static async createDetachedWindow(config?: Partial<DetachedWindowConfig>): Promise<chrome.windows.Window> {
|
||||||
|
// 检查是否已有打开的 detached 窗口
|
||||||
|
const existingWindowId = await this.getDetachedWindowId()
|
||||||
|
if (existingWindowId) {
|
||||||
|
const exists = await this.isWindowExists(existingWindowId)
|
||||||
|
if (exists) {
|
||||||
|
// 窗口已存在,聚焦到该窗口
|
||||||
|
const window = await chrome.windows.update(existingWindowId, { focused: true })
|
||||||
|
console.log('Detached 窗口已存在,已聚焦:', existingWindowId)
|
||||||
|
return window
|
||||||
|
} else {
|
||||||
|
// 窗口已关闭,清除记录
|
||||||
|
await this.clearDetachedWindowId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取保存的窗口位置
|
||||||
|
const savedPosition = await this.getSavedWindowPosition()
|
||||||
|
const position = config?.position || savedPosition
|
||||||
|
|
||||||
|
// 计算窗口位置(如果没有保存的位置,居中显示)
|
||||||
|
const windowConfig: chrome.windows.CreateData = {
|
||||||
|
url: config?.url || chrome.runtime.getURL('src/detached/index.html'),
|
||||||
|
type: 'popup', // 无地址栏的弹出窗口
|
||||||
|
width: position.width || DEFAULT_WINDOW_SIZE.width,
|
||||||
|
height: position.height || DEFAULT_WINDOW_SIZE.height,
|
||||||
|
focused: config?.focused !== false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有保存的位置,使用保存的位置
|
||||||
|
if (position.left !== undefined && position.top !== undefined) {
|
||||||
|
windowConfig.left = position.left
|
||||||
|
windowConfig.top = position.top
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建窗口
|
||||||
|
const newWindow = await chrome.windows.create(windowConfig)
|
||||||
|
|
||||||
|
if (newWindow.id) {
|
||||||
|
// 保存窗口 ID
|
||||||
|
await this.saveDetachedWindowId(newWindow.id)
|
||||||
|
|
||||||
|
// 监听窗口关闭事件,清除记录
|
||||||
|
chrome.windows.onRemoved.addListener(async (windowId) => {
|
||||||
|
if (windowId === newWindow.id) {
|
||||||
|
await this.clearDetachedWindowId()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('成功创建 detached 窗口:', newWindow.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 popup 弹出到独立窗口
|
||||||
|
*/
|
||||||
|
static async popoutToDetachedWindow(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 创建 detached 窗口
|
||||||
|
await this.createDetachedWindow()
|
||||||
|
|
||||||
|
// 关闭当前 popup
|
||||||
|
window.close()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('弹出到独立窗口失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存当前窗口位置(在 detached 窗口中调用)
|
||||||
|
*/
|
||||||
|
static async saveCurrentWindowPosition(): Promise<void> {
|
||||||
|
if (!this.isDetachedWindow()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentWindow = await chrome.windows.getCurrent()
|
||||||
|
if (currentWindow) {
|
||||||
|
const position: WindowPosition = {
|
||||||
|
left: currentWindow.left,
|
||||||
|
top: currentWindow.top,
|
||||||
|
width: currentWindow.width,
|
||||||
|
height: currentWindow.height,
|
||||||
|
}
|
||||||
|
await this.saveWindowPosition(position)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存当前窗口位置失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听窗口大小变化并自动保存位置(在 detached 窗口中调用)
|
||||||
|
*/
|
||||||
|
static setupWindowPositionTracking(): void {
|
||||||
|
if (!this.isDetachedWindow()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 debounce 避免频繁保存
|
||||||
|
let saveTimer: NodeJS.Timeout
|
||||||
|
const debouncedSave = () => {
|
||||||
|
clearTimeout(saveTimer)
|
||||||
|
saveTimer = setTimeout(() => {
|
||||||
|
this.saveCurrentWindowPosition()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听窗口大小和位置变化
|
||||||
|
chrome.windows.onBoundsChanged?.addListener(() => {
|
||||||
|
debouncedSave()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载或窗口关闭时保存
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
this.saveCurrentWindowPosition()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const windowManager = new WindowManager()
|
||||||
@ -9,6 +9,7 @@ export default defineConfig({
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
popup: resolve(__dirname, 'src/popup/index.html'),
|
popup: resolve(__dirname, 'src/popup/index.html'),
|
||||||
|
detached: resolve(__dirname, 'src/detached/index.html'),
|
||||||
options: resolve(__dirname, 'src/options/index.html'),
|
options: resolve(__dirname, 'src/options/index.html'),
|
||||||
background: resolve(__dirname, 'src/background/index.ts'),
|
background: resolve(__dirname, 'src/background/index.ts'),
|
||||||
content: resolve(__dirname, 'src/content/index.ts'),
|
content: resolve(__dirname, 'src/content/index.ts'),
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户