Skip to content

Commit 806b252

Browse files
committed
feat: improve notifications and bump version
1 parent d9fe228 commit 806b252

11 files changed

Lines changed: 255 additions & 83 deletions

File tree

desktop/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "nano-banana-pro-frontend",
33
"private": true,
4-
"version": "2.6.5",
4+
"version": "2.7.0",
55
"license": "MIT",
66
"description": "大香蕉图片生成工具 - 批量图片生成应用前端",
77
"type": "module",

desktop/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "desktop"
3-
version = "2.6.5"
3+
version = "2.7.0"
44
description = "A Tauri App"
55
authors = ["you"]
66
edition = "2021"

desktop/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "大香蕉 AI",
4-
"version": "2.6.5",
4+
"version": "2.7.0",
55
"identifier": "com.dztool.banana",
66
"build": {
77
"beforeDevCommand": "npm run dev",

desktop/src/components/Settings/SettingsModal.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { getSystemLocale } from '../../i18n/systemLocale';
1616
import appIcon from '../../assets/app-icon.png';
1717
import { IMAGE_MODEL_OPTIONS, VISION_MODEL_OPTIONS, CUSTOM_MODEL_VALUE } from '../../store/configStore';
1818
import { getPromptOptimizeConfigIssue } from '../../utils/promptOptimizeConfig';
19-
import { sendTestSystemNotification } from '../../hooks/useGenerationNotifications';
19+
import { ensureNotificationPermission, sendTestSystemNotification } from '../../hooks/useGenerationNotifications';
2020

2121
const CHAT_PROVIDER_OPTIONS = [
2222
{ value: 'gemini-chat', label: 'Gemini(/v1beta)', defaultBase: 'https://generativelanguage.googleapis.com' },
@@ -981,7 +981,14 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
981981
</div>
982982
<ToggleSwitch
983983
checked={draftEnableSystemNotifications}
984-
onChange={setDraftEnableSystemNotifications}
984+
onChange={(checked) => {
985+
setDraftEnableSystemNotifications(checked);
986+
if (!checked) return;
987+
void ensureNotificationPermission(t, {
988+
showDeniedToast: true,
989+
source: 'settings'
990+
});
991+
}}
985992
/>
986993
</div>
987994

desktop/src/hooks/useGenerationNotifications.ts

Lines changed: 180 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const getTaskPromptSummary = (task: HistoryItem) =>
1414
buildPromptNotificationSummary(task.promptOptimized, task.promptOriginal, task.prompt);
1515

1616
const shouldNotifyForStatus = (status: FinalTaskStatus, notifyOnFailure: boolean) => {
17-
if (status === 'failed') return notifyOnFailure;
17+
if (status === 'failed' || status === 'partial') return notifyOnFailure;
1818
return true;
1919
};
2020

@@ -54,6 +54,81 @@ const getNotificationCopy = (task: HistoryItem, t: (key: string, params?: Record
5454
};
5555
};
5656

57+
const getTaskSignature = (task: HistoryItem) => `${task.status}:${task.updatedAt || task.createdAt}`;
58+
59+
const getTaskNotificationPayload = (
60+
tasks: HistoryItem[],
61+
t: (key: string, params?: Record<string, unknown>) => string
62+
) => {
63+
const [firstTask] = tasks;
64+
const copy = getNotificationCopy(firstTask, t);
65+
if (tasks.length === 1) {
66+
return copy;
67+
}
68+
69+
const promptSummary = getTaskPromptSummary(firstTask);
70+
const additionalCount = tasks.length - 1;
71+
return {
72+
title: t(`notifications.status.${firstTask.status}.batchTitle`, { count: tasks.length }),
73+
body: promptSummary
74+
? t(`notifications.status.${firstTask.status}.batchBodyWithPrompt`, {
75+
prompt: promptSummary,
76+
count: tasks.length,
77+
additionalCount
78+
})
79+
: t(`notifications.status.${firstTask.status}.batchBody`, {
80+
count: tasks.length,
81+
additionalCount
82+
})
83+
};
84+
};
85+
86+
export async function ensureNotificationPermission(
87+
t: (key: string, params?: Record<string, unknown>) => string,
88+
options?: { showDeniedToast?: boolean; source?: 'startup' | 'settings' | 'test' | 'task' }
89+
) {
90+
const showDeniedToast = options?.showDeniedToast ?? true;
91+
const source = options?.source || 'startup';
92+
93+
const { isPermissionGranted, requestPermission } = await import('@tauri-apps/plugin-notification');
94+
let permissionGranted = await isPermissionGranted();
95+
console.info('[notification] permission state', { source, permissionGranted });
96+
if (!permissionGranted) {
97+
const permission = await requestPermission();
98+
console.info('[notification] requestPermission result', { source, permission });
99+
permissionGranted = permission === 'granted';
100+
}
101+
102+
if (!permissionGranted && showDeniedToast) {
103+
toast.info(t('settings.notifications.permissionDenied'));
104+
}
105+
106+
return permissionGranted;
107+
}
108+
109+
const getWindowVisibilitySnapshot = async () => {
110+
const visibilityState = typeof document !== 'undefined' ? document.visibilityState : 'visible';
111+
let focused = true;
112+
let visible = true;
113+
114+
try {
115+
const { getCurrentWindow } = await import('@tauri-apps/api/window');
116+
const appWindow = getCurrentWindow();
117+
[focused, visible] = await Promise.all([
118+
appWindow.isFocused().catch(() => false),
119+
appWindow.isVisible().catch(() => true)
120+
]);
121+
} catch (error) {
122+
console.warn('[notification] window visibility probe failed', error);
123+
}
124+
125+
return {
126+
visibilityState,
127+
focused,
128+
visible
129+
};
130+
};
131+
57132
export function useGenerationNotifications() {
58133
const { t } = useTranslation();
59134
const items = useHistoryStore((s) => s.items);
@@ -67,18 +142,6 @@ export function useGenerationNotifications() {
67142
const permissionRequestedRef = useRef(false);
68143
const permissionDeniedToastShownRef = useRef(false);
69144

70-
useEffect(() => {
71-
const nextMap = new Map<string, string>();
72-
for (const task of items) {
73-
if (task.status !== 'completed' && task.status !== 'failed' && task.status !== 'partial') {
74-
continue;
75-
}
76-
const signature = `${task.status}:${task.updatedAt || task.createdAt}`;
77-
nextMap.set(task.id, signature);
78-
}
79-
lastStatusSignatureRef.current = nextMap;
80-
}, [items]);
81-
82145
useEffect(() => {
83146
if (enableSystemNotifications && !previousEnabledRef.current) {
84147
enableBaselineRef.current = Date.now();
@@ -101,14 +164,10 @@ export function useGenerationNotifications() {
101164

102165
const ensurePermission = async () => {
103166
try {
104-
const { isPermissionGranted, requestPermission } = await import('@tauri-apps/plugin-notification');
105-
let permissionGranted = await isPermissionGranted();
106-
console.info('[notification] initial permission state', { permissionGranted });
107-
if (!permissionGranted) {
108-
const permission = await requestPermission();
109-
console.info('[notification] requestPermission result', { permission });
110-
permissionGranted = permission === 'granted';
111-
}
167+
const permissionGranted = await ensureNotificationPermission(t, {
168+
showDeniedToast: false,
169+
source: 'startup'
170+
});
112171
permissionRequestedRef.current = true;
113172
if (!permissionGranted) {
114173
if (!permissionDeniedToastShownRef.current) {
@@ -128,6 +187,7 @@ export function useGenerationNotifications() {
128187
if (!enableSystemNotifications) return;
129188
if (!isTauriRuntime()) return;
130189

190+
const signatureSnapshot = new Map(lastStatusSignatureRef.current);
131191
const finalTasks = [...items]
132192
.filter((task) => task.status === 'completed' || task.status === 'failed' || task.status === 'partial')
133193
.sort((a, b) => {
@@ -142,50 +202,111 @@ export function useGenerationNotifications() {
142202
return false;
143203
}
144204

145-
const currentSignature = `${task.status}:${task.updatedAt || task.createdAt}`;
146-
const previousSignature = lastStatusSignatureRef.current.get(task.id);
205+
const currentSignature = getTaskSignature(task);
206+
const previousSignature = signatureSnapshot.get(task.id);
147207
return currentSignature !== previousSignature;
148208
});
149209
if (!finalTasks.length) return;
150210

151211
const notify = async () => {
152212
try {
213+
const visibility = await getWindowVisibilitySnapshot();
214+
let { visibilityState, focused, visible } = visibility;
215+
153216
if (notifyOnlyWhenBackground) {
154-
const isVisible = typeof document !== 'undefined' ? document.visibilityState === 'visible' : true;
155-
const { getCurrentWindow } = await import('@tauri-apps/api/window');
156-
const appWindow = getCurrentWindow();
157-
const [focused, visible] = await Promise.all([
158-
appWindow.isFocused().catch(() => false),
159-
appWindow.isVisible().catch(() => true)
160-
]);
217+
const isVisible = visibilityState === 'visible';
161218
if (isVisible && focused && visible) {
219+
console.info('[notification] skipped: app visible in foreground', {
220+
taskIds: finalTasks.map((task) => task.id),
221+
statuses: finalTasks.map((task) => task.status),
222+
visibilityState,
223+
focused,
224+
visible
225+
});
162226
return;
163227
}
164228
}
165229

166-
const { isPermissionGranted, requestPermission, sendNotification } = await import('@tauri-apps/plugin-notification');
167-
let permissionGranted = await isPermissionGranted();
230+
const { sendNotification } = await import('@tauri-apps/plugin-notification');
231+
const permissionGranted = await ensureNotificationPermission(t, {
232+
showDeniedToast: false,
233+
source: 'task'
234+
});
168235
if (!permissionGranted) {
169-
const permission = await requestPermission();
170-
permissionGranted = permission === 'granted';
236+
console.info('[notification] skipped: permission not granted', {
237+
taskIds: finalTasks.map((task) => task.id),
238+
statuses: finalTasks.map((task) => task.status)
239+
});
240+
return;
171241
}
172-
if (!permissionGranted) return;
173242

174-
for (const task of finalTasks) {
243+
const tasksToNotify = finalTasks.filter((task) => {
175244
const finalStatus = task.status as FinalTaskStatus;
176-
if (!shouldNotifyForStatus(finalStatus, notifyOnFailure)) continue;
245+
if (!shouldNotifyForStatus(finalStatus, notifyOnFailure)) {
246+
signatureSnapshot.set(task.id, getTaskSignature(task));
247+
console.info('[notification] skipped: failure notifications disabled', {
248+
taskId: task.id,
249+
status: task.status
250+
});
251+
return false;
252+
}
177253

178254
const signature = `${task.id}:${finalStatus}:${task.updatedAt || task.createdAt}`;
179-
if (notifiedRef.current.has(signature)) continue;
255+
if (notifiedRef.current.has(signature)) {
256+
signatureSnapshot.set(task.id, getTaskSignature(task));
257+
console.info('[notification] skipped: already notified', {
258+
taskId: task.id,
259+
status: task.status
260+
});
261+
return false;
262+
}
263+
264+
return true;
265+
});
266+
267+
if (!tasksToNotify.length) {
268+
lastStatusSignatureRef.current = signatureSnapshot;
269+
return;
270+
}
180271

181-
const copy = getNotificationCopy(task, t);
272+
const groupedTasks = new Map<FinalTaskStatus, HistoryItem[]>();
273+
for (const task of tasksToNotify) {
274+
const status = task.status as FinalTaskStatus;
275+
const list = groupedTasks.get(status) || [];
276+
list.push(task);
277+
groupedTasks.set(status, list);
278+
}
279+
280+
const statusOrder: FinalTaskStatus[] = ['failed', 'partial', 'completed'];
281+
for (const status of statusOrder) {
282+
const tasks = groupedTasks.get(status);
283+
if (!tasks?.length) continue;
284+
285+
const payload = getTaskNotificationPayload(tasks, t);
182286
await sendNotification({
183-
title: copy.title,
184-
body: copy.body
287+
title: payload.title,
288+
body: payload.body
185289
});
186-
notifiedRef.current.add(signature);
187-
lastStatusSignatureRef.current.set(task.id, `${task.status}:${task.updatedAt || task.createdAt}`);
290+
console.info('[notification] task notification sent', {
291+
taskIds: tasks.map((task) => task.id),
292+
status,
293+
count: tasks.length,
294+
title: payload.title,
295+
body: payload.body,
296+
notifyOnlyWhenBackground,
297+
visibilityState,
298+
focused,
299+
visible
300+
});
301+
302+
for (const task of tasks) {
303+
const signature = `${task.id}:${status}:${task.updatedAt || task.createdAt}`;
304+
notifiedRef.current.add(signature);
305+
signatureSnapshot.set(task.id, getTaskSignature(task));
306+
}
188307
}
308+
309+
lastStatusSignatureRef.current = signatureSnapshot;
189310
} catch (error) {
190311
console.warn('[notification] send failed', error);
191312
}
@@ -197,24 +318,28 @@ export function useGenerationNotifications() {
197318

198319
export async function sendTestSystemNotification(t: (key: string, params?: Record<string, unknown>) => string) {
199320
try {
200-
const { isPermissionGranted, requestPermission, sendNotification } = await import('@tauri-apps/plugin-notification');
201-
let permissionGranted = await isPermissionGranted();
202-
console.info('[notification] test permission state', { permissionGranted });
203-
if (!permissionGranted) {
204-
const permission = await requestPermission();
205-
console.info('[notification] test requestPermission result', { permission });
206-
permissionGranted = permission === 'granted';
207-
}
321+
const { sendNotification } = await import('@tauri-apps/plugin-notification');
322+
const permissionGranted = await ensureNotificationPermission(t, {
323+
showDeniedToast: true,
324+
source: 'test'
325+
});
208326
if (!permissionGranted) {
209-
toast.info(t('settings.notifications.permissionDenied'));
210327
return;
211328
}
212329

330+
const visibility = await getWindowVisibilitySnapshot();
331+
const title = t('notifications.status.completed.title');
332+
const body = t('settings.notifications.testProbeBody');
333+
213334
await sendNotification({
214-
title: t('notifications.status.completed.title'),
215-
body: 'Notification permission probe'
335+
title,
336+
body
337+
});
338+
console.info('[notification] test notification sent', {
339+
title,
340+
body,
341+
...visibility
216342
});
217-
console.info('[notification] test notification sent');
218343
toast.success(t('settings.notifications.testSent'));
219344
} catch (error) {
220345
console.error('[notification] test notification failed', error);

0 commit comments

Comments
 (0)