fix(browser): 不可关闭、最小化可拖拽与重启
移除关闭入口;最小化为可拖动浮动条并持久化 minimized_rect;重启保留浏览历史。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -41,4 +41,4 @@
|
|||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|
||||||
浮动打开 → 最小化右下角 → 左下角展开 → 切换菜单 iframe 仍在。
|
浮动打开 → 最小化可拖动浮动条 → 左下角展开 → 不可关闭仅可重启 → 切换菜单 iframe 仍在。
|
||||||
|
|||||||
@@ -34,7 +34,13 @@ pytest tests/test_browser_ui_state.py -q
|
|||||||
cd frontend && npm run type-check
|
cd frontend && npm run type-check
|
||||||
```
|
```
|
||||||
|
|
||||||
左下角地球按钮打开;最小化后在右下角;历史按钮可见记录;换账号后记录隔离。
|
左下角地球按钮打开;最小化后为可拖动浮动条(默认右下角);历史按钮可见记录;换账号后记录隔离。
|
||||||
|
|
||||||
|
## 2026-06-11 更新 — 不可关闭、最小化可拖拽
|
||||||
|
|
||||||
|
- 移除「关闭浏览器」按钮;仅 **最小化** 或 **重启**(重启保留浏览历史)
|
||||||
|
- 最小化后为全局 **可拖动浮动条**(双击展开),位置持久化 `minimized_rect`
|
||||||
|
- 仅剩一个标签时不可关闭该标签
|
||||||
|
|
||||||
## 说明
|
## 说明
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
<v-tab v-for="tab in tabs" :key="tab.id" :value="tab.id" class="text-none px-2">
|
<v-tab v-for="tab in tabs" :key="tab.id" :value="tab.id" class="text-none px-2">
|
||||||
<span class="text-truncate" style="max-width: 120px">{{ tabLabel(tab) }}</span>
|
<span class="text-truncate" style="max-width: 120px">{{ tabLabel(tab) }}</span>
|
||||||
<v-btn
|
<v-btn
|
||||||
|
v-if="tabs.length > 1"
|
||||||
icon="mdi-close"
|
icon="mdi-close"
|
||||||
size="x-small"
|
size="x-small"
|
||||||
variant="text"
|
variant="text"
|
||||||
@@ -59,6 +60,16 @@
|
|||||||
@click="showHistory = !showHistory"
|
@click="showHistory = !showHistory"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
icon="mdi-restart"
|
||||||
|
title="重启浏览器(保留历史记录)"
|
||||||
|
density="compact"
|
||||||
|
@mousedown.stop
|
||||||
|
@click="onRestart"
|
||||||
|
/>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
size="small"
|
size="small"
|
||||||
variant="text"
|
variant="text"
|
||||||
@@ -71,20 +82,11 @@
|
|||||||
size="small"
|
size="small"
|
||||||
variant="text"
|
variant="text"
|
||||||
icon="mdi-window-minimize"
|
icon="mdi-window-minimize"
|
||||||
title="最小化到右下角"
|
title="最小化(可拖动)"
|
||||||
density="compact"
|
|
||||||
@mousedown.stop
|
|
||||||
@click="minimizePanel"
|
|
||||||
/>
|
|
||||||
<v-btn
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
icon="mdi-close"
|
|
||||||
title="关闭浏览器"
|
|
||||||
density="compact"
|
density="compact"
|
||||||
class="mr-1"
|
class="mr-1"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
@click="closePanel"
|
@click="minimizePanel"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -147,7 +149,7 @@
|
|||||||
<v-icon icon="mdi-web" size="48" class="mb-3 opacity-50" />
|
<v-icon icon="mdi-web" size="48" class="mb-3 opacity-50" />
|
||||||
<p class="text-body-2 text-center">输入地址后按回车或点「打开」</p>
|
<p class="text-body-2 text-center">输入地址后按回车或点「打开」</p>
|
||||||
<p class="text-caption mt-2 text-center">
|
<p class="text-caption mt-2 text-center">
|
||||||
页面在您当前的 Chrome / Firefox 等浏览器引擎中渲染;切换侧栏菜单时浏览器保持活动。
|
浏览器不可关闭,仅可最小化或重启;页面在您当前的 Chrome / Firefox 引擎中渲染。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -175,21 +177,43 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-card
|
<!-- Minimized — draggable floating bar (any corner) -->
|
||||||
|
<div
|
||||||
v-if="minimized && tabs.length"
|
v-if="minimized && tabs.length"
|
||||||
border
|
class="global-browser-mini-float"
|
||||||
rounded="lg"
|
:style="miniPanelStyle"
|
||||||
class="global-browser-dock-br"
|
|
||||||
>
|
>
|
||||||
<v-icon icon="mdi-web" size="18" class="mr-2" />
|
<v-card border rounded="lg" class="global-browser-mini-panel h-100 d-flex flex-column">
|
||||||
<span class="text-caption text-truncate global-browser-dock-label" @click="openPanel">
|
<div
|
||||||
{{ activeTab ? tabLabel(activeTab) : '浏览器' }}
|
class="global-browser-mini-toolbar d-flex align-center px-2 flex-grow-1 global-browser-toolbar--draggable"
|
||||||
</span>
|
@mousedown="onMiniToolbarMouseDown"
|
||||||
<v-chip v-if="tabs.length > 1" size="x-small" class="ml-2">{{ tabs.length }}</v-chip>
|
@dblclick="openPanel"
|
||||||
<v-spacer />
|
>
|
||||||
<v-btn size="x-small" variant="text" icon="mdi-dock-window" title="展开" @click="openPanel" />
|
<v-icon size="16" class="mr-1 text-disabled flex-shrink-0" @mousedown.stop>mdi-drag</v-icon>
|
||||||
<v-btn size="x-small" variant="text" icon="mdi-close" title="关闭" @click="closePanel" />
|
<v-icon icon="mdi-web" size="18" class="mr-2 flex-shrink-0" />
|
||||||
</v-card>
|
<span class="text-caption text-truncate flex-grow-1 min-w-0" @click="openPanel">
|
||||||
|
{{ activeTab ? tabLabel(activeTab) : '浏览器' }}
|
||||||
|
</span>
|
||||||
|
<v-chip v-if="tabs.length > 1" size="x-small" class="mx-1 flex-shrink-0">{{ tabs.length }}</v-chip>
|
||||||
|
<v-btn
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
icon="mdi-restart"
|
||||||
|
title="重启浏览器"
|
||||||
|
@mousedown.stop
|
||||||
|
@click.stop="onRestart"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
icon="mdi-dock-window"
|
||||||
|
title="展开"
|
||||||
|
@mousedown.stop
|
||||||
|
@click.stop="openPanel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="showLauncher"
|
v-if="showLauncher"
|
||||||
@@ -198,10 +222,22 @@
|
|||||||
icon="mdi-web"
|
icon="mdi-web"
|
||||||
size="large"
|
size="large"
|
||||||
elevation="4"
|
elevation="4"
|
||||||
title="打开 / 展开浏览器"
|
title="展开浏览器"
|
||||||
@click="onLauncherClick"
|
@click="onLauncherClick"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
|
<v-dialog v-model="showRestartConfirm" max-width="400">
|
||||||
|
<v-card border>
|
||||||
|
<v-card-title>重启浏览器?</v-card-title>
|
||||||
|
<v-card-text>将关闭所有标签并打开空白页,浏览历史记录会保留。</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="showRestartConfirm = false">取消</v-btn>
|
||||||
|
<v-btn color="primary" variant="flat" @click="confirmRestart">重启</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -212,6 +248,7 @@ import { useGlobalBrowser } from '@/composables/useGlobalBrowser'
|
|||||||
defineProps<{ showLauncher: boolean }>()
|
defineProps<{ showLauncher: boolean }>()
|
||||||
|
|
||||||
const showHistory = ref(false)
|
const showHistory = ref(false)
|
||||||
|
const showRestartConfirm = ref(false)
|
||||||
const maximized = ref(false)
|
const maximized = ref(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -224,12 +261,13 @@ const {
|
|||||||
activeTabId,
|
activeTabId,
|
||||||
activeTab,
|
activeTab,
|
||||||
panelRect,
|
panelRect,
|
||||||
|
minimizedRect,
|
||||||
canGoBack,
|
canGoBack,
|
||||||
canGoForward,
|
canGoForward,
|
||||||
tabLabel,
|
tabLabel,
|
||||||
openPanel,
|
openPanel,
|
||||||
minimizePanel,
|
minimizePanel,
|
||||||
closePanel,
|
restartBrowser,
|
||||||
navigate,
|
navigate,
|
||||||
refreshActive,
|
refreshActive,
|
||||||
goBack,
|
goBack,
|
||||||
@@ -240,21 +278,45 @@ const {
|
|||||||
newEmptyTab,
|
newEmptyTab,
|
||||||
openFromHistory,
|
openFromHistory,
|
||||||
savePanelRect,
|
savePanelRect,
|
||||||
|
saveMinimizedRect,
|
||||||
bootstrap,
|
bootstrap,
|
||||||
} = useGlobalBrowser()
|
} = useGlobalBrowser()
|
||||||
|
|
||||||
const fallback: FloatRect = { x: 80, y: 72, w: 980, h: 640 }
|
function defaultMiniRect(): FloatRect {
|
||||||
|
const w = 320
|
||||||
|
const h = 52
|
||||||
|
return {
|
||||||
|
x: Math.max(16, window.innerWidth - w - 16),
|
||||||
|
y: Math.max(16, window.innerHeight - h - 16),
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelFallback: FloatRect = { x: 80, y: 72, w: 980, h: 640 }
|
||||||
const { rect, panelStyle, startDrag, startResize, centerOnScreen } = useFloatingPanel(
|
const { rect, panelStyle, startDrag, startResize, centerOnScreen } = useFloatingPanel(
|
||||||
'nexus_global_browser_rect_v1',
|
'nexus_global_browser_rect_v1',
|
||||||
fallback,
|
panelFallback,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
rect: miniRect,
|
||||||
|
panelStyle: miniPanelStyle,
|
||||||
|
startDrag: startMiniDrag,
|
||||||
|
clampToViewport: clampMiniViewport,
|
||||||
|
} = useFloatingPanel('nexus_global_browser_mini_rect_v1', defaultMiniRect())
|
||||||
|
|
||||||
function seedRectFromServer() {
|
function seedRectFromServer() {
|
||||||
if (panelRect.value) {
|
if (panelRect.value) {
|
||||||
rect.value = { ...panelRect.value }
|
rect.value = { ...panelRect.value }
|
||||||
return
|
} else {
|
||||||
|
centerOnScreen()
|
||||||
|
}
|
||||||
|
if (minimizedRect.value) {
|
||||||
|
miniRect.value = { ...minimizedRect.value }
|
||||||
|
} else {
|
||||||
|
miniRect.value = defaultMiniRect()
|
||||||
}
|
}
|
||||||
centerOnScreen()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -266,11 +328,21 @@ watch(rect, (value) => {
|
|||||||
savePanelRect({ ...value })
|
savePanelRect({ ...value })
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
|
watch(miniRect, (value) => {
|
||||||
|
saveMinimizedRect({ ...value })
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
function onToolbarMouseDown(e: MouseEvent) {
|
function onToolbarMouseDown(e: MouseEvent) {
|
||||||
if (maximized.value) return
|
if (maximized.value) return
|
||||||
startDrag(e)
|
startDrag(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onMiniToolbarMouseDown(e: MouseEvent) {
|
||||||
|
if (e.button !== 0) return
|
||||||
|
startMiniDrag(e)
|
||||||
|
clampMiniViewport()
|
||||||
|
}
|
||||||
|
|
||||||
function onResizeStart(e: MouseEvent) {
|
function onResizeStart(e: MouseEvent) {
|
||||||
startResize(e)
|
startResize(e)
|
||||||
}
|
}
|
||||||
@@ -290,10 +362,20 @@ function onLauncherClick() {
|
|||||||
}
|
}
|
||||||
newEmptyTab()
|
newEmptyTab()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onRestart() {
|
||||||
|
showRestartConfirm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmRestart() {
|
||||||
|
showRestartConfirm.value = false
|
||||||
|
restartBrowser()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.global-browser-float {
|
.global-browser-float,
|
||||||
|
.global-browser-mini-float {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 2350;
|
z-index: 2350;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -307,19 +389,27 @@ function onLauncherClick() {
|
|||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
top: 0 !important;
|
top: 0 !important;
|
||||||
}
|
}
|
||||||
.global-browser-panel {
|
.global-browser-panel,
|
||||||
|
.global-browser-mini-panel {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgb(var(--v-theme-surface));
|
background: rgb(var(--v-theme-surface));
|
||||||
}
|
}
|
||||||
.global-browser-toolbar {
|
.global-browser-toolbar,
|
||||||
|
.global-browser-mini-toolbar {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
}
|
}
|
||||||
|
.global-browser-mini-toolbar {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
.global-browser-toolbar--draggable {
|
.global-browser-toolbar--draggable {
|
||||||
cursor: move;
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
.global-browser-nav {
|
.global-browser-nav {
|
||||||
background: rgb(var(--v-theme-surface));
|
background: rgb(var(--v-theme-surface));
|
||||||
@@ -363,22 +453,6 @@ function onLauncherClick() {
|
|||||||
rgba(var(--v-theme-on-surface), 0.25) 50%
|
rgba(var(--v-theme-on-surface), 0.25) 50%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.global-browser-dock-br {
|
|
||||||
position: fixed;
|
|
||||||
right: 16px;
|
|
||||||
bottom: 16px;
|
|
||||||
z-index: 2340;
|
|
||||||
max-width: min(420px, calc(100vw - 120px));
|
|
||||||
padding: 8px 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
|
|
||||||
}
|
|
||||||
.global-browser-dock-label {
|
|
||||||
max-width: 200px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.global-browser-launcher {
|
.global-browser-launcher {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface BrowserStateResponse {
|
|||||||
active_tab_id: string | null
|
active_tab_id: string | null
|
||||||
minimized: boolean
|
minimized: boolean
|
||||||
rect: FloatRect | null
|
rect: FloatRect | null
|
||||||
|
minimized_rect: FloatRect | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const bootstrapped = ref(false)
|
const bootstrapped = ref(false)
|
||||||
@@ -52,6 +53,7 @@ const visits = ref<BrowserVisit[]>([])
|
|||||||
const tabs = ref<BrowserTab[]>([])
|
const tabs = ref<BrowserTab[]>([])
|
||||||
const activeTabId = ref<string | null>(null)
|
const activeTabId = ref<string | null>(null)
|
||||||
const panelRect = ref<FloatRect | null>(null)
|
const panelRect = ref<FloatRect | null>(null)
|
||||||
|
const minimizedRect = ref<FloatRect | null>(null)
|
||||||
|
|
||||||
let saveTimer: ReturnType<typeof setTimeout> | null = null
|
let saveTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
@@ -98,6 +100,7 @@ function applyServerState(data: BrowserStateResponse) {
|
|||||||
}
|
}
|
||||||
minimized.value = Boolean(data.minimized)
|
minimized.value = Boolean(data.minimized)
|
||||||
panelRect.value = data.rect
|
panelRect.value = data.rect
|
||||||
|
minimizedRect.value = data.minimized_rect
|
||||||
panelOpen.value = tabs.value.length > 0 && !minimized.value
|
panelOpen.value = tabs.value.length > 0 && !minimized.value
|
||||||
syncAddressFromActive()
|
syncAddressFromActive()
|
||||||
}
|
}
|
||||||
@@ -136,6 +139,7 @@ async function persistState(extra?: { recordUrl?: string; recordTitle?: string }
|
|||||||
active_tab_id: activeTabId.value,
|
active_tab_id: activeTabId.value,
|
||||||
minimized: minimized.value,
|
minimized: minimized.value,
|
||||||
rect: panelRect.value,
|
rect: panelRect.value,
|
||||||
|
minimized_rect: minimizedRect.value,
|
||||||
}
|
}
|
||||||
if (extra?.recordUrl) {
|
if (extra?.recordUrl) {
|
||||||
body.record_url = extra.recordUrl
|
body.record_url = extra.recordUrl
|
||||||
@@ -200,13 +204,21 @@ function minimizePanel() {
|
|||||||
scheduleSave()
|
scheduleSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePanel() {
|
/** Reset to a single blank tab; browsing history is kept. */
|
||||||
tabs.value = []
|
function restartBrowser() {
|
||||||
activeTabId.value = null
|
const tab: BrowserTab = {
|
||||||
panelOpen.value = false
|
id: newTabId(),
|
||||||
minimized.value = false
|
url: '',
|
||||||
maximized.value = false
|
title: '新标签',
|
||||||
|
stack: [],
|
||||||
|
stackIndex: -1,
|
||||||
|
frameKey: 0,
|
||||||
|
}
|
||||||
|
tabs.value = [tab]
|
||||||
|
activeTabId.value = tab.id
|
||||||
addressDraft.value = ''
|
addressDraft.value = ''
|
||||||
|
lastError.value = null
|
||||||
|
openPanel()
|
||||||
scheduleSave()
|
scheduleSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,6 +301,7 @@ function switchTab(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeTab(id: string) {
|
function closeTab(id: string) {
|
||||||
|
if (tabs.value.length <= 1) return
|
||||||
const idx = tabs.value.findIndex((t) => t.id === id)
|
const idx = tabs.value.findIndex((t) => t.id === id)
|
||||||
if (idx < 0) return
|
if (idx < 0) return
|
||||||
const next = tabs.value.filter((t) => t.id !== id)
|
const next = tabs.value.filter((t) => t.id !== id)
|
||||||
@@ -296,10 +309,6 @@ function closeTab(id: string) {
|
|||||||
if (activeTabId.value === id) {
|
if (activeTabId.value === id) {
|
||||||
activeTabId.value = next[idx]?.id ?? next[idx - 1]?.id ?? null
|
activeTabId.value = next[idx]?.id ?? next[idx - 1]?.id ?? null
|
||||||
}
|
}
|
||||||
if (!next.length) {
|
|
||||||
closePanel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
syncAddressFromActive()
|
syncAddressFromActive()
|
||||||
scheduleSave()
|
scheduleSave()
|
||||||
}
|
}
|
||||||
@@ -329,6 +338,11 @@ function savePanelRect(rect: FloatRect) {
|
|||||||
scheduleSave()
|
scheduleSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveMinimizedRect(rect: FloatRect) {
|
||||||
|
minimizedRect.value = rect
|
||||||
|
scheduleSave()
|
||||||
|
}
|
||||||
|
|
||||||
export function useGlobalBrowser() {
|
export function useGlobalBrowser() {
|
||||||
return {
|
return {
|
||||||
bootstrapped,
|
bootstrapped,
|
||||||
@@ -344,6 +358,7 @@ export function useGlobalBrowser() {
|
|||||||
activeTabId,
|
activeTabId,
|
||||||
activeTab,
|
activeTab,
|
||||||
panelRect,
|
panelRect,
|
||||||
|
minimizedRect,
|
||||||
isActive,
|
isActive,
|
||||||
canGoBack,
|
canGoBack,
|
||||||
canGoForward,
|
canGoForward,
|
||||||
@@ -351,7 +366,7 @@ export function useGlobalBrowser() {
|
|||||||
bootstrap,
|
bootstrap,
|
||||||
openPanel,
|
openPanel,
|
||||||
minimizePanel,
|
minimizePanel,
|
||||||
closePanel,
|
restartBrowser,
|
||||||
openUrl,
|
openUrl,
|
||||||
navigate,
|
navigate,
|
||||||
refreshActive,
|
refreshActive,
|
||||||
@@ -363,6 +378,7 @@ export function useGlobalBrowser() {
|
|||||||
newEmptyTab,
|
newEmptyTab,
|
||||||
openFromHistory,
|
openFromHistory,
|
||||||
savePanelRect,
|
savePanelRect,
|
||||||
|
saveMinimizedRect,
|
||||||
scheduleSave,
|
scheduleSave,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class BrowserStateUpdate(BaseModel):
|
|||||||
active_tab_id: str | None = None
|
active_tab_id: str | None = None
|
||||||
minimized: bool | None = None
|
minimized: bool | None = None
|
||||||
rect: BrowserPanelRect | None = None
|
rect: BrowserPanelRect | None = None
|
||||||
|
minimized_rect: BrowserPanelRect | None = None
|
||||||
record_url: str | None = Field(default=None, description="Append one visit + history entry")
|
record_url: str | None = Field(default=None, description="Append one visit + history entry")
|
||||||
record_title: str | None = None
|
record_title: str | None = None
|
||||||
|
|
||||||
@@ -75,6 +76,8 @@ async def put_browser_state(
|
|||||||
incoming["tabs"] = [t.model_dump(exclude_none=True) for t in payload.tabs]
|
incoming["tabs"] = [t.model_dump(exclude_none=True) for t in payload.tabs]
|
||||||
if payload.rect is not None:
|
if payload.rect is not None:
|
||||||
incoming["rect"] = payload.rect.model_dump()
|
incoming["rect"] = payload.rect.model_dump()
|
||||||
|
if payload.minimized_rect is not None:
|
||||||
|
incoming["minimized_rect"] = payload.minimized_rect.model_dump()
|
||||||
|
|
||||||
merged = merge_browser_state(current, incoming)
|
merged = merge_browser_state(current, incoming)
|
||||||
if payload.record_url:
|
if payload.record_url:
|
||||||
|
|||||||
@@ -43,9 +43,24 @@ def empty_browser_state() -> dict[str, Any]:
|
|||||||
"active_tab_id": None,
|
"active_tab_id": None,
|
||||||
"minimized": False,
|
"minimized": False,
|
||||||
"rect": None,
|
"rect": None,
|
||||||
|
"minimized_rect": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_rect(raw: dict | None, *, default_w: float, default_h: float) -> dict[str, float] | None:
|
||||||
|
if raw is None or not isinstance(raw, dict):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
"x": float(raw.get("x", 0)),
|
||||||
|
"y": float(raw.get("y", 0)),
|
||||||
|
"w": float(raw.get("w", default_w)),
|
||||||
|
"h": float(raw.get("h", default_h)),
|
||||||
|
}
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def parse_browser_state(raw: dict | None) -> dict[str, Any]:
|
def parse_browser_state(raw: dict | None) -> dict[str, Any]:
|
||||||
base = empty_browser_state()
|
base = empty_browser_state()
|
||||||
if not raw or not isinstance(raw, dict):
|
if not raw or not isinstance(raw, dict):
|
||||||
@@ -78,20 +93,26 @@ def parse_browser_state(raw: dict | None) -> dict[str, Any]:
|
|||||||
url = item.get("url")
|
url = item.get("url")
|
||||||
if not isinstance(tab_id, str) or not tab_id.strip():
|
if not isinstance(tab_id, str) or not tab_id.strip():
|
||||||
continue
|
continue
|
||||||
if not isinstance(url, str) or not _is_safe_url(url):
|
if not isinstance(url, str):
|
||||||
continue
|
continue
|
||||||
stack = [
|
if url.strip() and not _is_safe_url(url):
|
||||||
u for u in item.get("stack") or [url]
|
continue
|
||||||
if isinstance(u, str) and _is_safe_url(u)
|
if url.strip():
|
||||||
] or [url]
|
stack = [
|
||||||
stack_index = item.get("stack_index", len(stack) - 1)
|
u for u in item.get("stack") or [url]
|
||||||
if not isinstance(stack_index, int):
|
if isinstance(u, str) and _is_safe_url(u)
|
||||||
stack_index = len(stack) - 1
|
] or [url]
|
||||||
stack_index = max(0, min(stack_index, len(stack) - 1))
|
stack_index = item.get("stack_index", len(stack) - 1)
|
||||||
|
if not isinstance(stack_index, int):
|
||||||
|
stack_index = len(stack) - 1
|
||||||
|
stack_index = max(0, min(stack_index, len(stack) - 1))
|
||||||
|
else:
|
||||||
|
stack = []
|
||||||
|
stack_index = -1
|
||||||
tabs.append({
|
tabs.append({
|
||||||
"id": tab_id.strip()[:64],
|
"id": tab_id.strip()[:64],
|
||||||
"url": url,
|
"url": url.strip(),
|
||||||
"title": str(item.get("title") or _hostname(url))[:200],
|
"title": str(item.get("title") or (_hostname(url) if url.strip() else "新标签"))[:200],
|
||||||
"stack": stack[-64:],
|
"stack": stack[-64:],
|
||||||
"stack_index": stack_index,
|
"stack_index": stack_index,
|
||||||
})
|
})
|
||||||
@@ -101,19 +122,8 @@ def parse_browser_state(raw: dict | None) -> dict[str, Any]:
|
|||||||
if not isinstance(active_tab_id, str) or not any(t["id"] == active_tab_id for t in tabs):
|
if not isinstance(active_tab_id, str) or not any(t["id"] == active_tab_id for t in tabs):
|
||||||
active_tab_id = tabs[0]["id"] if tabs else None
|
active_tab_id = tabs[0]["id"] if tabs else None
|
||||||
|
|
||||||
rect = raw.get("rect")
|
rect = _parse_rect(raw.get("rect"), default_w=960, default_h=640)
|
||||||
if rect is not None and not isinstance(rect, dict):
|
minimized_rect = _parse_rect(raw.get("minimized_rect"), default_w=320, default_h=52)
|
||||||
rect = None
|
|
||||||
if isinstance(rect, dict):
|
|
||||||
try:
|
|
||||||
rect = {
|
|
||||||
"x": float(rect.get("x", 0)),
|
|
||||||
"y": float(rect.get("y", 0)),
|
|
||||||
"w": float(rect.get("w", 960)),
|
|
||||||
"h": float(rect.get("h", 640)),
|
|
||||||
}
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
rect = None
|
|
||||||
|
|
||||||
minimized = bool(raw.get("minimized"))
|
minimized = bool(raw.get("minimized"))
|
||||||
|
|
||||||
@@ -124,6 +134,7 @@ def parse_browser_state(raw: dict | None) -> dict[str, Any]:
|
|||||||
"active_tab_id": active_tab_id,
|
"active_tab_id": active_tab_id,
|
||||||
"minimized": minimized,
|
"minimized": minimized,
|
||||||
"rect": rect,
|
"rect": rect,
|
||||||
|
"minimized_rect": minimized_rect,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user