Skip to content

Commit 4d07314

Browse files
CopilotLuSrackhall
andcommitted
Implement export flow signature integration (T034, T037)
Co-authored-by: LuSrackhall <142690689+LuSrackhall@users.noreply.github.com>
1 parent 746e418 commit 4d07314

File tree

3 files changed

+255
-2
lines changed

3 files changed

+255
-2
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<!--
2+
* This file is part of the KeyTone project.
3+
*
4+
* Copyright (C) 2024 LuSrackhall
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
-->
19+
20+
<template>
21+
<q-dialog v-model="showDialog" persistent>
22+
<q-card style="max-width: 360px; width: 100%">
23+
<q-card-section>
24+
<div class="text-h6">{{ $t('signature.exportFlow.selectSignature') }}</div>
25+
</q-card-section>
26+
27+
<q-separator />
28+
29+
<q-card-section>
30+
<div class="text-body2 q-mb-md">{{ $t('signature.exportFlow.selectPrompt') }}</div>
31+
32+
<q-select
33+
v-model="selectedSignature"
34+
:options="signatureOptions"
35+
:label="$t('signature.signatureName')"
36+
outlined
37+
dense
38+
option-label="name"
39+
option-value="name"
40+
:error="required && !selectedSignature"
41+
:error-message="$t('signature.exportFlow.signatureRequired')"
42+
>
43+
<template v-slot:no-option>
44+
<q-item>
45+
<q-item-section class="text-grey">
46+
{{ $t('signature.emptyState.noSignatures') }}
47+
</q-item-section>
48+
</q-item>
49+
</template>
50+
</q-select>
51+
</q-card-section>
52+
53+
<q-separator />
54+
55+
<q-card-actions align="right">
56+
<q-btn flat :label="$t('KeyToneAlbum.cancel')" color="primary" @click="handleCancel" />
57+
<q-btn
58+
flat
59+
:label="$t('KeyToneAlbum.confirm')"
60+
color="primary"
61+
@click="handleConfirm"
62+
:disable="required && !selectedSignature"
63+
/>
64+
</q-card-actions>
65+
</q-card>
66+
</q-dialog>
67+
</template>
68+
69+
<script setup lang="ts">
70+
import { ref, onMounted } from 'vue';
71+
import { useQuasar } from 'quasar';
72+
import { useI18n } from 'vue-i18n';
73+
import { api } from 'src/boot/axios';
74+
75+
const $q = useQuasar();
76+
const { t } = useI18n();
77+
78+
const showDialog = ref(false);
79+
const selectedSignature = ref<any>(null);
80+
const signatureOptions = ref<any[]>([]);
81+
const required = ref(false);
82+
83+
let resolveCallback: ((signature: any) => void) | null = null;
84+
let rejectCallback: (() => void) | null = null;
85+
86+
// Load signatures from backend
87+
const loadSignatures = async () => {
88+
try {
89+
const response = await api.get('/store/get', {
90+
params: { key: 'signature_manager' }
91+
});
92+
93+
if (response.data.message === 'ok' && response.data.value) {
94+
const signatureManager = response.data.value;
95+
signatureOptions.value = Object.values(signatureManager).filter((v: any) => v && v.name);
96+
} else {
97+
signatureOptions.value = [];
98+
}
99+
} catch (error) {
100+
console.error('Failed to load signatures:', error);
101+
signatureOptions.value = [];
102+
}
103+
};
104+
105+
// Open the dialog
106+
const open = (isRequired: boolean = false): Promise<any> => {
107+
return new Promise((resolve, reject) => {
108+
required.value = isRequired;
109+
selectedSignature.value = null;
110+
resolveCallback = resolve;
111+
rejectCallback = reject;
112+
showDialog.value = true;
113+
loadSignatures();
114+
});
115+
};
116+
117+
// Handle confirm
118+
const handleConfirm = () => {
119+
if (required.value && !selectedSignature.value) {
120+
$q.notify({
121+
type: 'warning',
122+
message: t('signature.exportFlow.signatureRequired'),
123+
});
124+
return;
125+
}
126+
127+
showDialog.value = false;
128+
if (resolveCallback) {
129+
resolveCallback(selectedSignature.value);
130+
}
131+
};
132+
133+
// Handle cancel
134+
const handleCancel = () => {
135+
showDialog.value = false;
136+
if (rejectCallback) {
137+
rejectCallback();
138+
}
139+
};
140+
141+
// Expose open method
142+
defineExpose({ open });
143+
</script>
144+
145+
<style scoped>
146+
.q-card {
147+
border-radius: 8px;
148+
}
149+
</style>

frontend/src/pages/Keytone_album_page_new.vue

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@
284284

285285
<!-- Signature Management Dialog -->
286286
<SignatureManagementDialog ref="signatureDialogRef" />
287+
288+
<!-- Signature Select Dialog for Export -->
289+
<SignatureSelectDialog ref="signatureSelectDialogRef" />
287290
</template>
288291

289292
<script setup lang="ts">
@@ -294,8 +297,10 @@ import { nanoid } from 'nanoid';
294297
import { computed, nextTick, useTemplateRef } from 'vue';
295298
import KeytoneAlbum from 'src/components/Keytone_album.vue';
296299
import SignatureManagementDialog from 'src/components/SignatureManagementDialog.vue';
300+
import SignatureSelectDialog from 'src/components/SignatureSelectDialog.vue';
297301
import { ref, onMounted, onUnmounted, watch } from 'vue';
298302
import { useI18n } from 'vue-i18n';
303+
import { api } from 'src/boot/axios';
299304
import {
300305
DeleteAlbum,
301306
GetAudioPackageName,
@@ -325,6 +330,7 @@ const keytoneAlbum_store = useKeytoneAlbumStore();
325330
const selectedKeyTonePkgRef = useTemplateRef<QSelect>('selectedKeyTonePkgRef');
326331
const keytoneAlbumRef = ref<InstanceType<typeof KeytoneAlbum> | null>(null);
327332
const signatureDialogRef = ref<InstanceType<typeof SignatureManagementDialog> | null>(null);
333+
const signatureSelectDialogRef = ref<InstanceType<typeof SignatureSelectDialog> | null>(null);
328334
const isCollapsed = ref(false);
329335
let lastScrollTop = 0;
330336
const isAtTop = ref(true);
@@ -413,6 +419,55 @@ const exportAlbumLegacy = async () => {
413419
}
414420
const albumName = albumNameResponse.name;
415421
422+
// Check if album has signatures - get album config to see if it has signatures
423+
let selectedSignature = null;
424+
try {
425+
const albumConfigResponse = await api.get('/keytone_pkg/get', {
426+
params: {
427+
key: 'album_signatures',
428+
audioPkgUUID: albumNameResponse.audioPkgUUID || setting_store.mainHome.selectedKeyTonePkg.split(/[\\/]/).pop()
429+
}
430+
});
431+
432+
const hasSignatures = albumConfigResponse.data.value && Object.keys(albumConfigResponse.data.value).length > 0;
433+
434+
// Show signature selection dialog
435+
try {
436+
selectedSignature = await signatureSelectDialogRef.value?.open(hasSignatures);
437+
} catch (err) {
438+
// User cancelled signature selection
439+
return;
440+
}
441+
442+
// If album has signatures but no signature selected, prevent export
443+
if (hasSignatures && !selectedSignature) {
444+
q.notify({
445+
type: 'warning',
446+
message: $t('signature.exportFlow.signatureRequired'),
447+
});
448+
return;
449+
}
450+
} catch (error) {
451+
console.log('No signatures check or album config not found, proceeding without signature selection');
452+
}
453+
454+
// If signature was selected, call the sign bridge
455+
if (selectedSignature) {
456+
try {
457+
await api.post('/export/sign-bridge', {
458+
albumId: albumNameResponse.audioPkgUUID || setting_store.mainHome.selectedKeyTonePkg.split(/[\\/]/).pop(),
459+
signatureName: selectedSignature.name
460+
});
461+
} catch (error) {
462+
console.error('Failed to add signature to album:', error);
463+
q.notify({
464+
type: 'warning',
465+
message: $t('signature.notify.exportFailed') + ': ' + (error instanceof Error ? error.message : String(error)),
466+
});
467+
// Continue with export even if signature bridge fails
468+
}
469+
}
470+
416471
// 调用导出函数获取zip文件blob
417472
const blob = await ExportAlbum(setting_store.mainHome.selectedKeyTonePkg);
418473
const url = window.URL.createObjectURL(blob);
@@ -456,6 +511,55 @@ const exportAlbum = async () => {
456511
}
457512
const albumName = albumNameResponse.name;
458513
514+
// Check if album has signatures - get album config to see if it has signatures
515+
let selectedSignature = null;
516+
try {
517+
const albumConfigResponse = await api.get('/keytone_pkg/get', {
518+
params: {
519+
key: 'album_signatures',
520+
audioPkgUUID: albumNameResponse.audioPkgUUID || setting_store.mainHome.selectedKeyTonePkg.split(/[\\/]/).pop()
521+
}
522+
});
523+
524+
const hasSignatures = albumConfigResponse.data.value && Object.keys(albumConfigResponse.data.value).length > 0;
525+
526+
// Show signature selection dialog
527+
try {
528+
selectedSignature = await signatureSelectDialogRef.value?.open(hasSignatures);
529+
} catch (err) {
530+
// User cancelled signature selection
531+
return;
532+
}
533+
534+
// If album has signatures but no signature selected, prevent export
535+
if (hasSignatures && !selectedSignature) {
536+
q.notify({
537+
type: 'warning',
538+
message: $t('signature.exportFlow.signatureRequired'),
539+
});
540+
return;
541+
}
542+
} catch (error) {
543+
console.log('No signatures check or album config not found, proceeding without signature selection');
544+
}
545+
546+
// If signature was selected, call the sign bridge
547+
if (selectedSignature) {
548+
try {
549+
await api.post('/export/sign-bridge', {
550+
albumId: albumNameResponse.audioPkgUUID || setting_store.mainHome.selectedKeyTonePkg.split(/[\\/]/).pop(),
551+
signatureName: selectedSignature.name
552+
});
553+
} catch (error) {
554+
console.error('Failed to add signature to album:', error);
555+
q.notify({
556+
type: 'warning',
557+
message: $t('signature.notify.exportFailed') + ': ' + (error instanceof Error ? error.message : String(error)),
558+
});
559+
// Continue with export even if signature bridge fails
560+
}
561+
}
562+
459563
// 获取导出数据
460564
const blob = await ExportAlbum(setting_store.mainHome.selectedKeyTonePkg);
461565

specs/002-aaaa-aaaa-aaaa/tasks.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@
5050
- [x] T031 创建与删除:通过 `/store/set` 更新 `signature_manager`;保护码由前端创建时自动生成且 UI 不展示(frontend/src/services/ 或 stores/)
5151
- [x] T032 导出签名文件(.ktsign):调用 POST `/signature/export`,并处理保存(前端桥接到系统对话框,遵循 Electron 安全边界);注意:该操作为“签名文件管理”,不涉及“专辑导出”
5252
- [x] T033 导入签名:调用 POST `/signature/import`,成功后列表自动刷新(SSE)
53-
- [ ] T034 导出流程集成:在专辑导出步骤弹出签名选择,调用 `/export/sign-bridge` 将签名写入 `album_signatures` 并继续导出(frontend/src/pages/ 或组件)
53+
- [x] T034 导出流程集成:在专辑导出步骤弹出签名选择,调用 `/export/sign-bridge` 将签名写入 `album_signatures` 并继续导出(frontend/src/pages/ 或组件)
5454
- [x] T035 i18n:新增 `signature` 命名空间的中英文 key,覆盖所有用户可见文本(frontend/src/i18n/{zh-CN,en-US}/index.json)
5555
- [x] T036 覆盖确认 UI:导入签名遇到“唯一标识相同”的重复时,弹出“覆盖/取消”确认对话框并完成 i18n 覆盖
56-
- [ ] T037 导出前置校验:当专辑已有签名时,导出流程必须要求再选择签名(否则禁用继续/提示);与后端桥接响应保持一致的错误提示
56+
- [x] T037 导出前置校验:当专辑已有签名时,导出流程必须要求再选择签名(否则禁用继续/提示);与后端桥接响应保持一致的错误提示
5757

5858
### 3.4.x 追加(一致性与可用性)
5959

0 commit comments

Comments
 (0)