Skip to content

Commit 6022f63

Browse files
author
Hongzhi Wen
committed
Merge remote-tracking branch 'origin/main' into return-ball-drag-recovery
# Conflicts: # static/app-ui.js
2 parents d8f7008 + 32322f4 commit 6022f63

31 files changed

Lines changed: 1309 additions & 209 deletions

docs/design/compact-chat-mode-design.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@
126126
- 监听 `neko:compact-surface-drag-grab`(来自 React 工具轮盘原点拖拽),非 Electron 时以事件坐标为锚启动 compact surface 本体拖拽(复用既有 startDrag/全局 mousemove/mouseup 与落点 click 守卫)。Electron 由 `preload-chat-react.js` 监听同一事件改走原生窗口拖拽。
127127
5. `static/app-buttons.js` 是发送桥之一。compact history 文本发送必须带清晰 session / request 语义,不能让已有 composer 附件在 deferred send 中被误带上。
128128
6. 语音模式 / `composerHidden` 下的 history drop 只保留前端拖拽、命中和收束动效;真实发送必须在 `sendCompactHistoryDropPayload` 边界跳过,不能通过改 React 拖拽 phase 或样式来伪装。
129-
7. `static/music_ui.js` 的音乐播放器优先挂到打开且可交互的 `.compact-export-history-music-mount`历史关闭或卸载后应回落到 composer 内唯一 `#music-player-mount`。不要为了保住音乐挂载点让整个历史消息面板长期常驻
129+
7. `static/music_ui.js` 的音乐播放器在 compact 模式下优先挂到常驻 `.compact-music-player-mount#music-player-mount`历史关闭或卸载不能把播放器挪回 composer fallback,但播放器视觉显隐必须跟随历史打开、closing、closed 状态,也不能被通用 `#music-player-mount` 样式撑成超过 compact surface 的横向尺寸;音量弹层展开/收起时必须刷新 compact geometry,避免浮出播放器原生矩形的滑块看得见但不可点
130130

131131
### NEKO-PC 桌面壳
132132

@@ -368,12 +368,12 @@ Compact 历史默认在初次启动时显示。历史列表本身由常驻展开
368368
8. 常驻 `.compact-history-visibility-handle` 只控制历史列表显隐;它关闭历史时不清除操作栏打开状态。
369369
9. 工具转轮历史/导出按钮控制操作栏显示;如果历史关闭时点击该按钮,应先打开历史并显示操作栏。
370370
10. 历史关闭后可以播放 closing 动画;动画结束后历史面板必须卸载。关闭期间新增的文字/语音消息不得进入历史面板 DOM,也不得在历史区域短暂闪现;重新打开历史时再按最新 `messages` 完整渲染。
371-
11. closing 期间历史气泡、操作栏、音乐 mount 和预览控件都不进入 history hit region,不保留按钮语义、键盘焦点或透明命中区。
371+
11. closing 期间历史气泡、操作栏和预览控件都不进入 history hit region,不保留按钮语义、键盘焦点或透明命中区。
372372
12. 操作栏显示期间进入选择模式:气泡点击 / 键盘 Enter / Space 可以选中或取消选中历史消息。
373373
13. 操作栏隐藏时退出选择模式:必须清空当前选中项,并禁止继续通过点击或键盘选择;拖拽源识别和拖拽发送不受这个选择模式限制。
374374
14. 操作栏包含选择和导出动作,如计数、全选、取消/清空、反选、导出预览等;操作栏自身进入 history hit region。
375375
15. 选择状态、导出预览和操作栏显示状态由 React state 管理;操作栏状态可以跨历史显隐保留,但只在历史实际打开时算作可见。
376-
16. 历史面板内有专用 `.compact-export-history-music-mount`只在历史打开且可交互时作为音乐播放器优先挂载点;关闭/卸载后播放器必须回落到 composer 的 `#music-player-mount`
376+
16. 音乐播放器有独立 `.compact-music-player-mount#music-player-mount`它与历史消息面板分离并作为 `musicPlayer` 几何项进入 compact surface;历史关闭/卸载后播放器必须继续停留在该独立挂载点,但视觉上要随历史一起收起和展开;横向尺寸必须限制在 compact surface 宽度内;历史记录底部必须为播放器高度和阴影预留间距,不能与播放器重叠;音量滑块展开到播放器外侧时要触发 geometry refresh
377377
17. 预览关闭时要清理 stale export error 和必要 preview lifecycle 状态,避免重新打开显示旧错误。
378378
18. 历史透明区域不能长期遮挡后方;可见气泡、按钮、预览控件和必要滚动区域可命中,气泡间透明区应尽量穿透。
379379
19. GalGame / ChoicePrompt 出现时,选项层在历史层上方。
@@ -581,7 +581,7 @@ Surface:
581581
7. 历史透明区不遮挡后方。
582582
8. 历史关闭动画结束后,历史面板卸载;关闭期间继续发生文字/语音对话时,历史区域不出现新气泡闪现。
583583
9. 历史重新展开后,关闭期间产生的新消息会按最新 `messages` 正常出现在历史中。
584-
10. 播放中的音乐栏在历史打开时可挂到历史面板,历史关闭/卸载后能回落到 composer 挂载点
584+
10. 播放中的音乐栏在 compact 模式下停留在独立播放器挂载点;历史打开时显示,历史 closing / closed 时同步收起且不再命中;历史打开、关闭或卸载都不能把它挪回 composer,横向宽度不能突破 compact surface,历史记录不能贴住或覆盖播放器;音量滑块展开后可以点击和拖拽
585585
11. 预览关闭不会保留旧 error。
586586

587587
历史拖拽:

frontend/plugin-manager/env.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,19 @@ declare module 'element-plus/dist/locale/ko.mjs'
88
declare module 'element-plus/dist/locale/ru.mjs'
99
declare module 'element-plus/dist/locale/es.mjs'
1010
declare module 'element-plus/dist/locale/pt.mjs'
11+
12+
interface NekoWindowControlResult {
13+
ok?: boolean
14+
isMaximized?: boolean
15+
}
16+
17+
interface NekoWindowControlApi {
18+
minimize?: () => Promise<unknown> | unknown
19+
restore?: () => Promise<unknown> | unknown
20+
maximize?: () => Promise<NekoWindowControlResult> | NekoWindowControlResult
21+
isMaximized?: () => Promise<boolean> | boolean
22+
}
23+
24+
interface Window {
25+
nekoWindowControl?: NekoWindowControlApi
26+
}

frontend/plugin-manager/src/components/layout/AppLayout.vue

Lines changed: 142 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,37 @@
55
<img src="@/assets/paw.png" alt="" class="titlebar-paw" draggable="false" />
66
<span class="titlebar-text">{{ t('app.titleSuffix') }}</span>
77
</div>
8-
<button
9-
class="titlebar-close"
10-
type="button"
11-
:title="t('common.close')"
12-
:aria-label="t('common.close')"
13-
@click="closeWindow"
14-
>
15-
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
16-
<path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
17-
</svg>
18-
</button>
8+
<div class="titlebar-controls">
9+
<button
10+
class="titlebar-control"
11+
type="button"
12+
:title="t('common.minimize')"
13+
:aria-label="t('common.minimize')"
14+
@click="minimizeWindow"
15+
>
16+
<span class="titlebar-minimize-icon" aria-hidden="true"></span>
17+
</button>
18+
<button
19+
class="titlebar-control"
20+
type="button"
21+
:title="maximizeLabel"
22+
:aria-label="maximizeLabel"
23+
@click="toggleMaximize"
24+
>
25+
<span class="titlebar-maximize-icon" :class="{ 'is-restored': isMaximized }" aria-hidden="true"></span>
26+
</button>
27+
<button
28+
class="titlebar-control titlebar-control--close"
29+
type="button"
30+
:title="t('common.close')"
31+
:aria-label="t('common.close')"
32+
@click="closeWindow"
33+
>
34+
<svg class="titlebar-close-icon" viewBox="0 0 10 10" fill="none" aria-hidden="true" focusable="false">
35+
<path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
36+
</svg>
37+
</button>
38+
</div>
1939
</div>
2040

2141
<div class="app-shell">
@@ -26,7 +46,7 @@
2646
<div class="app-body">
2747
<div v-if="connectionStore.disconnected" class="connection-banner">
2848
<div class="connection-banner__inner">
29-
⚠️ {{ t('common.disconnected') }}
49+
{{ t('common.disconnected') }}
3050
</div>
3151
</div>
3252

@@ -47,17 +67,72 @@
4767
</template>
4868

4969
<script setup lang="ts">
70+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
5071
import Sidebar from './Sidebar.vue'
5172
import Header from './Header.vue'
5273
import { useI18n } from 'vue-i18n'
5374
import { useConnectionStore } from '@/stores/connection'
5475
5576
const { t } = useI18n()
5677
const connectionStore = useConnectionStore()
78+
const isMaximized = ref(false)
79+
const maximizeLabel = computed(() => isMaximized.value ? t('common.restore') : t('common.maximize'))
80+
81+
function getWindowControlApi() {
82+
return window.nekoWindowControl
83+
}
84+
85+
async function refreshMaximizeState() {
86+
const api = getWindowControlApi()
87+
if (!api || typeof api.isMaximized !== 'function') return
88+
try {
89+
isMaximized.value = !!(await api.isMaximized())
90+
} catch {
91+
// 非桌面窗口环境下忽略状态查询失败
92+
}
93+
}
94+
95+
async function minimizeWindow() {
96+
const api = getWindowControlApi()
97+
if (!api || typeof api.minimize !== 'function') return
98+
try {
99+
await api.minimize()
100+
} catch {
101+
// 非桌面窗口环境下忽略最小化失败
102+
}
103+
}
104+
105+
async function toggleMaximize() {
106+
const api = getWindowControlApi()
107+
if (!api || typeof api.maximize !== 'function') return
108+
try {
109+
const result = await api.maximize()
110+
if (result && result.ok && typeof result.isMaximized === 'boolean') {
111+
isMaximized.value = result.isMaximized
112+
return
113+
}
114+
await refreshMaximizeState()
115+
} catch {
116+
// 非桌面窗口环境下忽略最大化失败
117+
}
118+
}
119+
120+
function handleWindowResize() {
121+
void refreshMaximizeState()
122+
}
57123
58124
function closeWindow() {
59125
window.close()
60126
}
127+
128+
onMounted(() => {
129+
void refreshMaximizeState()
130+
window.addEventListener('resize', handleWindowResize)
131+
})
132+
133+
onBeforeUnmount(() => {
134+
window.removeEventListener('resize', handleWindowResize)
135+
})
61136
</script>
62137

63138
<style scoped>
@@ -68,7 +143,7 @@ function closeWindow() {
68143
overflow: hidden;
69144
}
70145
71-
/* ── Title bar (acrylic) ── */
146+
/* 标题栏玻璃效果 */
72147
.window-titlebar {
73148
padding: 0 6px 0 12px;
74149
height: 38px;
@@ -79,7 +154,6 @@ function closeWindow() {
79154
-webkit-app-region: drag;
80155
user-select: none;
81156
z-index: 9999;
82-
/* Acrylic effect — matches react-neko-chat topbar */
83157
background:
84158
linear-gradient(135deg,
85159
rgba(75, 212, 253, 0.82) 0%,
@@ -117,7 +191,14 @@ function closeWindow() {
117191
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
118192
}
119193
120-
.titlebar-close {
194+
.titlebar-controls {
195+
display: flex;
196+
align-items: center;
197+
gap: 4px;
198+
-webkit-app-region: no-drag;
199+
}
200+
201+
.titlebar-control {
121202
-webkit-app-region: no-drag;
122203
background: transparent;
123204
border: none;
@@ -132,16 +213,56 @@ function closeWindow() {
132213
transition: background 0.18s, color 0.18s;
133214
}
134215
135-
.titlebar-close:hover {
216+
.titlebar-control:hover {
136217
background: rgba(255, 255, 255, 0.18);
137218
color: #fff;
138219
}
139220
140-
.titlebar-close:active {
221+
.titlebar-control:active {
141222
background: rgba(0, 0, 0, 0.08);
142223
}
143224
144-
/* ── Shell layout ── */
225+
.titlebar-control--close:hover {
226+
background: rgba(255, 255, 255, 0.22);
227+
}
228+
229+
.titlebar-minimize-icon {
230+
width: 12px;
231+
height: 1.5px;
232+
border-radius: 999px;
233+
background: currentColor;
234+
transform: translateY(4px);
235+
}
236+
237+
.titlebar-maximize-icon {
238+
position: relative;
239+
width: 11px;
240+
height: 11px;
241+
border: 1.5px solid currentColor;
242+
border-radius: 2px;
243+
}
244+
245+
.titlebar-maximize-icon.is-restored {
246+
transform: translate(1.5px, 1.5px);
247+
}
248+
249+
.titlebar-maximize-icon.is-restored::before {
250+
content: '';
251+
position: absolute;
252+
left: -4px;
253+
top: -4px;
254+
width: 11px;
255+
height: 11px;
256+
border: 1.5px solid currentColor;
257+
border-radius: 2px;
258+
}
259+
260+
.titlebar-close-icon {
261+
width: 10px;
262+
height: 10px;
263+
}
264+
265+
/* 主体布局 */
145266
.app-shell {
146267
flex: 1;
147268
display: flex;
@@ -188,7 +309,7 @@ function closeWindow() {
188309
background: var(--el-bg-color-page);
189310
}
190311
191-
/* ── Connection banner ── */
312+
/* 连接状态提示 */
192313
.connection-banner {
193314
padding: 8px 20px 0;
194315
}
@@ -203,7 +324,7 @@ function closeWindow() {
203324
font-weight: 500;
204325
}
205326
206-
/* ── Page transition ── */
327+
/* 页面切换动画 */
207328
.page-enter-active {
208329
transition:
209330
opacity 0.3s cubic-bezier(0.22, 1, 0.36, 1),
@@ -230,7 +351,7 @@ function closeWindow() {
230351
filter: blur(2px);
231352
}
232353
233-
/* ── Dark mode acrylic overrides ── */
354+
/* 深色模式覆盖 */
234355
html.dark .window-titlebar {
235356
background:
236357
linear-gradient(135deg,

frontend/plugin-manager/src/i18n/locales/en-US.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export default {
1717
back: 'Back',
1818
submit: 'Submit',
1919
close: 'Close',
20+
minimize: 'Minimize',
21+
maximize: 'Maximize',
22+
restore: 'Restore',
2023
toggleSelection: 'Toggle selection',
2124
success: 'Success',
2225
error: 'Error',

frontend/plugin-manager/src/i18n/locales/es.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export default {
1717
back: 'Atrás',
1818
submit: 'Enviar',
1919
close: 'Cerrar',
20+
minimize: 'Minimizar',
21+
maximize: 'Maximizar',
22+
restore: 'Restaurar',
2023
toggleSelection: 'Alternar selección',
2124
success: 'Éxito',
2225
error: 'Error',

frontend/plugin-manager/src/i18n/locales/ja.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export default {
1717
back: '戻る',
1818
submit: '送信',
1919
close: '閉じる',
20+
minimize: '最小化',
21+
maximize: '最大化',
22+
restore: '復元',
2023
toggleSelection: '選択を切り替え',
2124
success: '成功',
2225
error: 'エラー',

frontend/plugin-manager/src/i18n/locales/ko.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export default {
1717
back: '뒤로',
1818
submit: '제출',
1919
close: '닫기',
20+
minimize: '최소화',
21+
maximize: '최대화',
22+
restore: '복원',
2023
toggleSelection: '선택 전환',
2124
success: '성공',
2225
error: '오류',

frontend/plugin-manager/src/i18n/locales/pt.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export default {
1717
back: 'Voltar',
1818
submit: 'Enviar',
1919
close: 'Fechar',
20+
minimize: 'Minimizar',
21+
maximize: 'Maximizar',
22+
restore: 'Restaurar',
2023
toggleSelection: 'Alternar seleção',
2124
success: 'Sucesso',
2225
error: 'Erro',

frontend/plugin-manager/src/i18n/locales/ru.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export default {
1717
back: 'Назад',
1818
submit: 'Отправить',
1919
close: 'Закрыть',
20+
minimize: 'Свернуть',
21+
maximize: 'Развернуть',
22+
restore: 'Восстановить',
2023
toggleSelection: 'Переключить выбор',
2124
success: 'Успешно',
2225
error: 'Ошибка',

frontend/plugin-manager/src/i18n/locales/zh-CN.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export default {
1717
back: '返回',
1818
submit: '提交',
1919
close: '关闭',
20+
minimize: '最小化',
21+
maximize: '最大化',
22+
restore: '恢复',
2023
toggleSelection: '切换选中状态',
2124
success: '成功',
2225
error: '错误',

0 commit comments

Comments
 (0)