electron-notify-manager 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +323 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/src/NotificationManager.d.ts +45 -0
- package/dist/src/NotificationManager.d.ts.map +1 -0
- package/dist/src/NotificationManager.js +337 -0
- package/dist/src/NotificationManager.js.map +1 -0
- package/dist/src/NotificationWindow.d.ts +19 -0
- package/dist/src/NotificationWindow.d.ts.map +1 -0
- package/dist/src/NotificationWindow.js +165 -0
- package/dist/src/NotificationWindow.js.map +1 -0
- package/dist/src/constants.d.ts +22 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +39 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/core/NotificationQueue.d.ts +14 -0
- package/dist/src/core/NotificationQueue.d.ts.map +1 -0
- package/dist/src/core/NotificationQueue.js +35 -0
- package/dist/src/core/NotificationQueue.js.map +1 -0
- package/dist/src/errors.d.ts +23 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +43 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/ipc/IpcChannels.d.ts +9 -0
- package/dist/src/ipc/IpcChannels.d.ts.map +1 -0
- package/dist/src/ipc/IpcChannels.js +11 -0
- package/dist/src/ipc/IpcChannels.js.map +1 -0
- package/dist/src/position/PositionCalculator.d.ts +22 -0
- package/dist/src/position/PositionCalculator.d.ts.map +1 -0
- package/dist/src/position/PositionCalculator.js +77 -0
- package/dist/src/position/PositionCalculator.js.map +1 -0
- package/dist/src/positionCalculator.d.ts +19 -0
- package/dist/src/positionCalculator.d.ts.map +1 -0
- package/dist/src/positionCalculator.js +50 -0
- package/dist/src/positionCalculator.js.map +1 -0
- package/dist/src/types/config.types.d.ts +11 -0
- package/dist/src/types/config.types.d.ts.map +1 -0
- package/dist/src/types/config.types.js +3 -0
- package/dist/src/types/config.types.js.map +1 -0
- package/dist/src/types/index.d.ts +8 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +3 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/ipc.types.d.ts +12 -0
- package/dist/src/types/ipc.types.d.ts.map +1 -0
- package/dist/src/types/ipc.types.js +3 -0
- package/dist/src/types/ipc.types.js.map +1 -0
- package/dist/src/types/notification.types.d.ts +28 -0
- package/dist/src/types/notification.types.d.ts.map +1 -0
- package/dist/src/types/notification.types.js +3 -0
- package/dist/src/types/notification.types.js.map +1 -0
- package/dist/src/types/position.types.d.ts +12 -0
- package/dist/src/types/position.types.d.ts.map +1 -0
- package/dist/src/types/position.types.js +3 -0
- package/dist/src/types/position.types.js.map +1 -0
- package/dist/src/types/theme.types.d.ts +8 -0
- package/dist/src/types/theme.types.d.ts.map +1 -0
- package/dist/src/types/theme.types.js +3 -0
- package/dist/src/types/theme.types.js.map +1 -0
- package/dist/src/utils/idGenerator.d.ts +7 -0
- package/dist/src/utils/idGenerator.d.ts.map +1 -0
- package/dist/src/utils/idGenerator.js +16 -0
- package/dist/src/utils/idGenerator.js.map +1 -0
- package/dist/src/utils/logger.d.ts +12 -0
- package/dist/src/utils/logger.d.ts.map +1 -0
- package/dist/src/utils/logger.js +38 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/themeDetector.d.ts +3 -0
- package/dist/src/utils/themeDetector.d.ts.map +1 -0
- package/dist/src/utils/themeDetector.js +11 -0
- package/dist/src/utils/themeDetector.js.map +1 -0
- package/dist/src/utils/validators.d.ts +10 -0
- package/dist/src/utils/validators.d.ts.map +1 -0
- package/dist/src/utils/validators.js +95 -0
- package/dist/src/utils/validators.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +50 -0
- package/renderer/notification.css +172 -0
- package/renderer/notification.html +52 -0
- package/renderer/notification.js +137 -0
- package/renderer/notification.ts +150 -0
- package/renderer/preload.js +38 -0
- package/renderer/preload.ts +56 -0
- package/renderer/scripts/animationController.ts +12 -0
- package/renderer/scripts/icons.ts +36 -0
- package/renderer/scripts/notification.js +188 -0
- package/renderer/scripts/progressBar.ts +10 -0
- package/renderer/styles/animations.css +28 -0
- package/renderer/styles/notification.css +186 -0
- package/renderer/styles/themes.css +94 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
type CloseReason = 'timeout' | 'manual';
|
|
2
|
+
|
|
3
|
+
interface RepositionPayload {
|
|
4
|
+
id: string;
|
|
5
|
+
y: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface RendererApi {
|
|
9
|
+
sendClose: (id: string) => void;
|
|
10
|
+
sendClick: (id: string) => void;
|
|
11
|
+
onReposition: (handler: (payload: RepositionPayload) => void) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
interface Window {
|
|
16
|
+
electronNotify: RendererApi;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getParam(key: string, fallback = ''): string {
|
|
21
|
+
const sp = new URLSearchParams(window.location.search);
|
|
22
|
+
const v = sp.get(key);
|
|
23
|
+
return v === null ? fallback : v;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getNumberParam(key: string, fallback: number): number {
|
|
27
|
+
const raw = getParam(key, '');
|
|
28
|
+
const n = Number(raw);
|
|
29
|
+
return Number.isFinite(n) ? n : fallback;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const id = getParam('id', '');
|
|
33
|
+
const title = getParam('title', '');
|
|
34
|
+
const description = getParam('description', '');
|
|
35
|
+
const image = getParam('image', '');
|
|
36
|
+
const duration = Math.max(0, Math.floor(getNumberParam('duration', 4000)));
|
|
37
|
+
|
|
38
|
+
const root = document.getElementById('root');
|
|
39
|
+
const titleEl = document.getElementById('title');
|
|
40
|
+
const descEl = document.getElementById('desc');
|
|
41
|
+
const closeBtn = document.getElementById('closeBtn');
|
|
42
|
+
const imgEl = document.getElementById('image');
|
|
43
|
+
const defaultIconEl = document.getElementById('defaultIcon');
|
|
44
|
+
const progressEl = document.getElementById('progress');
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
root === null ||
|
|
48
|
+
titleEl === null ||
|
|
49
|
+
descEl === null ||
|
|
50
|
+
closeBtn === null ||
|
|
51
|
+
imgEl === null ||
|
|
52
|
+
defaultIconEl === null ||
|
|
53
|
+
progressEl === null
|
|
54
|
+
) {
|
|
55
|
+
throw new Error('Notification UI elements missing from notification.html');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
titleEl.textContent = title;
|
|
59
|
+
descEl.textContent = description;
|
|
60
|
+
|
|
61
|
+
const hasImage = typeof image === 'string' && image.trim().length > 0;
|
|
62
|
+
if (hasImage) {
|
|
63
|
+
(imgEl as HTMLImageElement).src = image;
|
|
64
|
+
(imgEl as HTMLElement).style.display = 'block';
|
|
65
|
+
(defaultIconEl as HTMLElement).style.display = 'none';
|
|
66
|
+
} else {
|
|
67
|
+
(imgEl as HTMLImageElement).removeAttribute('src');
|
|
68
|
+
(imgEl as HTMLElement).style.display = 'none';
|
|
69
|
+
(defaultIconEl as HTMLElement).style.display = 'block';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let closing = false;
|
|
73
|
+
let autoTimer: number | null = null;
|
|
74
|
+
|
|
75
|
+
function enter(): void {
|
|
76
|
+
requestAnimationFrame(() => {
|
|
77
|
+
root.classList.remove('entering');
|
|
78
|
+
root.classList.add('entered');
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function requestClose(_reason: CloseReason): void {
|
|
83
|
+
if (closing) return;
|
|
84
|
+
closing = true;
|
|
85
|
+
|
|
86
|
+
if (autoTimer !== null) {
|
|
87
|
+
window.clearTimeout(autoTimer);
|
|
88
|
+
autoTimer = null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
root.classList.remove('entered');
|
|
92
|
+
root.classList.add('exiting');
|
|
93
|
+
|
|
94
|
+
window.setTimeout(() => {
|
|
95
|
+
window.electronNotify.sendClose(id);
|
|
96
|
+
}, 220);
|
|
97
|
+
|
|
98
|
+
(root as HTMLElement).style.pointerEvents = 'none';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function startProgress(): void {
|
|
102
|
+
if (duration <= 0) {
|
|
103
|
+
(progressEl as HTMLElement).style.opacity = '0';
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
(progressEl as HTMLElement).style.opacity = '1';
|
|
108
|
+
progressEl.animate([{ transform: 'scaleX(1)' }, { transform: 'scaleX(0)' }], {
|
|
109
|
+
duration,
|
|
110
|
+
easing: 'linear',
|
|
111
|
+
fill: 'forwards'
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
autoTimer = window.setTimeout(() => requestClose('timeout'), duration);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
closeBtn.addEventListener('click', (e: MouseEvent) => {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
e.stopPropagation();
|
|
120
|
+
requestClose('manual');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
root.addEventListener('click', () => {
|
|
124
|
+
if (closing) return;
|
|
125
|
+
window.electronNotify.sendClick(id);
|
|
126
|
+
requestClose('manual');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
root.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
130
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
(root as HTMLElement).click();
|
|
133
|
+
} else if (e.key === 'Escape') {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
requestClose('manual');
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
window.electronNotify.onReposition((_payload) => {
|
|
140
|
+
root.animate([{ transform: 'translateY(-2px)' }, { transform: 'translateY(0px)' }], {
|
|
141
|
+
duration: 240,
|
|
142
|
+
easing: 'ease-out'
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
enter();
|
|
147
|
+
startProgress();
|
|
148
|
+
|
|
149
|
+
export {};
|
|
150
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
5
|
+
|
|
6
|
+
contextBridge.exposeInMainWorld('electronNotify', {
|
|
7
|
+
sendClose: (id) => ipcRenderer.send('notification:close', id),
|
|
8
|
+
sendClick: (id) => ipcRenderer.send('notification:click', id),
|
|
9
|
+
onReposition: (handler) => {
|
|
10
|
+
ipcRenderer.on('notification:reposition', (_event, payload) => {
|
|
11
|
+
if (!payload || typeof payload !== 'object') return;
|
|
12
|
+
if (typeof payload.id !== 'string') return;
|
|
13
|
+
if (typeof payload.y !== 'number') return;
|
|
14
|
+
handler({ id: payload.id, y: payload.y });
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
onUpdate: (handler) => {
|
|
18
|
+
ipcRenderer.on('notification:update', (_event, payload) => {
|
|
19
|
+
if (!payload || typeof payload !== 'object') return;
|
|
20
|
+
if (typeof payload.id !== 'string') return;
|
|
21
|
+
if (!payload.updates || typeof payload.updates !== 'object') return;
|
|
22
|
+
const u = payload.updates;
|
|
23
|
+
const updates = {};
|
|
24
|
+
if (typeof u.progress === 'number') updates.progress = u.progress;
|
|
25
|
+
if (typeof u.loadingText === 'string') updates.loadingText = u.loadingText;
|
|
26
|
+
if (typeof u.description === 'string') updates.description = u.description;
|
|
27
|
+
handler({ id: payload.id, updates });
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
onTheme: (handler) => {
|
|
31
|
+
ipcRenderer.on('notification:theme', (_event, payload) => {
|
|
32
|
+
if (!payload || typeof payload !== 'object') return;
|
|
33
|
+
if (payload.theme !== 'dark' && payload.theme !== 'light') return;
|
|
34
|
+
handler({ theme: payload.theme });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { contextBridge, ipcRenderer } from 'electron';
|
|
2
|
+
|
|
3
|
+
const IPC_CHANNELS = {
|
|
4
|
+
NOTIFICATION_CLOSE: 'notification:close',
|
|
5
|
+
NOTIFICATION_CLICK: 'notification:click',
|
|
6
|
+
NOTIFICATION_REPOSITION: 'notification:reposition',
|
|
7
|
+
NOTIFICATION_UPDATE: 'notification:update',
|
|
8
|
+
NOTIFICATION_THEME: 'notification:theme',
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export interface RendererApi {
|
|
12
|
+
sendClose: (id: string) => void;
|
|
13
|
+
sendClick: (id: string) => void;
|
|
14
|
+
onReposition: (handler: (payload: { id: string; y: number }) => void) => void;
|
|
15
|
+
onUpdate: (handler: (payload: { id: string; updates: { progress?: number; loadingText?: string; description?: string } }) => void) => void;
|
|
16
|
+
onTheme: (handler: (payload: { theme: 'dark' | 'light' }) => void) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const api: RendererApi = {
|
|
20
|
+
sendClose: (id: string) => ipcRenderer.send(IPC_CHANNELS.NOTIFICATION_CLOSE, id),
|
|
21
|
+
sendClick: (id: string) => ipcRenderer.send(IPC_CHANNELS.NOTIFICATION_CLICK, id),
|
|
22
|
+
onReposition: (handler) => {
|
|
23
|
+
ipcRenderer.on(IPC_CHANNELS.NOTIFICATION_REPOSITION, (_event, payload: unknown) => {
|
|
24
|
+
if (typeof payload !== 'object' || payload === null) return;
|
|
25
|
+
const maybe = payload as { id?: unknown; y?: unknown };
|
|
26
|
+
if (typeof maybe.id !== 'string') return;
|
|
27
|
+
if (typeof maybe.y !== 'number') return;
|
|
28
|
+
handler({ id: maybe.id, y: maybe.y });
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
onUpdate: (handler) => {
|
|
32
|
+
ipcRenderer.on(IPC_CHANNELS.NOTIFICATION_UPDATE, (_event, payload: unknown) => {
|
|
33
|
+
if (typeof payload !== 'object' || payload === null) return;
|
|
34
|
+
const maybe = payload as { id?: unknown; updates?: unknown };
|
|
35
|
+
if (typeof maybe.id !== 'string') return;
|
|
36
|
+
if (typeof maybe.updates !== 'object' || maybe.updates === null) return;
|
|
37
|
+
const u = maybe.updates as { progress?: unknown; loadingText?: unknown; description?: unknown };
|
|
38
|
+
const updates: { progress?: number; loadingText?: string; description?: string } = {};
|
|
39
|
+
if (typeof u.progress === 'number') updates.progress = u.progress;
|
|
40
|
+
if (typeof u.loadingText === 'string') updates.loadingText = u.loadingText;
|
|
41
|
+
if (typeof u.description === 'string') updates.description = u.description;
|
|
42
|
+
handler({ id: maybe.id, updates });
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
onTheme: (handler) => {
|
|
46
|
+
ipcRenderer.on(IPC_CHANNELS.NOTIFICATION_THEME, (_event, payload: unknown) => {
|
|
47
|
+
if (typeof payload !== 'object' || payload === null) return;
|
|
48
|
+
const maybe = payload as { theme?: unknown };
|
|
49
|
+
if (maybe.theme !== 'dark' && maybe.theme !== 'light') return;
|
|
50
|
+
handler({ theme: maybe.theme });
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
contextBridge.exposeInMainWorld('electronNotify', api);
|
|
56
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function enter(el: HTMLElement): void {
|
|
2
|
+
requestAnimationFrame(() => {
|
|
3
|
+
el.classList.add('entered');
|
|
4
|
+
el.classList.remove('entering');
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function exit(el: HTMLElement): void {
|
|
9
|
+
el.classList.add('exiting');
|
|
10
|
+
el.classList.remove('entered');
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type NotificationVariant =
|
|
2
|
+
| 'default'
|
|
3
|
+
| 'success'
|
|
4
|
+
| 'error'
|
|
5
|
+
| 'warning'
|
|
6
|
+
| 'loading'
|
|
7
|
+
| 'progress';
|
|
8
|
+
|
|
9
|
+
export const ICONS: Record<NotificationVariant, string> = {
|
|
10
|
+
default: `<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
11
|
+
<path d="M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z" stroke="currentColor" stroke-width="2"/>
|
|
12
|
+
<path d="M12 10v7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
13
|
+
<path d="M12 7h.01" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
|
|
14
|
+
</svg>`,
|
|
15
|
+
success: `<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
16
|
+
<path d="M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z" stroke="currentColor" stroke-width="2"/>
|
|
17
|
+
<path d="M7.5 12.5l3 3 6-7" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
18
|
+
</svg>`,
|
|
19
|
+
error: `<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
20
|
+
<path d="M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z" stroke="currentColor" stroke-width="2"/>
|
|
21
|
+
<path d="M8.5 8.5l7 7M15.5 8.5l-7 7" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
|
22
|
+
</svg>`,
|
|
23
|
+
warning: `<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
24
|
+
<path d="M12 3l10 18H2L12 3z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
|
25
|
+
<path d="M12 9v5" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
|
26
|
+
<path d="M12 17h.01" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
|
|
27
|
+
</svg>`,
|
|
28
|
+
loading: `<svg class="spin" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
29
|
+
<path d="M12 3a9 9 0 1 0 9 9" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
|
30
|
+
</svg>`,
|
|
31
|
+
progress: `<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
32
|
+
<path d="M4 12a8 8 0 1 0 8-8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
33
|
+
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
34
|
+
</svg>`,
|
|
35
|
+
};
|
|
36
|
+
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
function getParam(key, fallback = '') {
|
|
5
|
+
const sp = new URLSearchParams(window.location.search);
|
|
6
|
+
const v = sp.get(key);
|
|
7
|
+
return v == null ? fallback : v;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getNumberParam(key, fallback) {
|
|
11
|
+
const raw = getParam(key, '');
|
|
12
|
+
const n = Number(raw);
|
|
13
|
+
return Number.isFinite(n) ? n : fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function clampProgress(value) {
|
|
17
|
+
if (Number.isNaN(value)) return 0;
|
|
18
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ICONS = {
|
|
22
|
+
default: `<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
23
|
+
<path d="M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z" stroke="currentColor" stroke-width="2"/>
|
|
24
|
+
<path d="M12 10v7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
25
|
+
<path d="M12 7h.01" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
|
|
26
|
+
</svg>`,
|
|
27
|
+
success: `<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
28
|
+
<path d="M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z" stroke="currentColor" stroke-width="2"/>
|
|
29
|
+
<path d="M7.5 12.5l3 3 6-7" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
30
|
+
</svg>`,
|
|
31
|
+
error: `<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
32
|
+
<path d="M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z" stroke="currentColor" stroke-width="2"/>
|
|
33
|
+
<path d="M8.5 8.5l7 7M15.5 8.5l-7 7" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
|
34
|
+
</svg>`,
|
|
35
|
+
warning: `<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
36
|
+
<path d="M12 3l10 18H2L12 3z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
|
37
|
+
<path d="M12 9v5" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
|
38
|
+
<path d="M12 17h.01" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
|
|
39
|
+
</svg>`,
|
|
40
|
+
loading: `<svg class="spin" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
41
|
+
<path d="M12 3a9 9 0 1 0 9 9" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
|
42
|
+
</svg>`,
|
|
43
|
+
progress: `<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
44
|
+
<path d="M4 12a8 8 0 1 0 8-8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
45
|
+
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
46
|
+
</svg>`
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const id = getParam('id', '');
|
|
50
|
+
const title = getParam('title', '');
|
|
51
|
+
const description = getParam('description', '');
|
|
52
|
+
const image = getParam('image', '');
|
|
53
|
+
const duration = Math.max(0, Math.floor(getNumberParam('duration', 4000)));
|
|
54
|
+
const variant = getParam('variant', 'default');
|
|
55
|
+
const themeResolved = getParam('themeResolved', 'dark');
|
|
56
|
+
const progress = getNumberParam('progress', 0);
|
|
57
|
+
const progressLabel = getParam('progressLabel', '');
|
|
58
|
+
const loadingText = getParam('loadingText', '');
|
|
59
|
+
|
|
60
|
+
const root = document.getElementById('notification');
|
|
61
|
+
const titleEl = document.getElementById('title');
|
|
62
|
+
const descEl = document.getElementById('desc');
|
|
63
|
+
const closeBtn = document.getElementById('closeBtn');
|
|
64
|
+
const iconSvg = document.getElementById('iconSvg');
|
|
65
|
+
const imgEl = document.getElementById('image');
|
|
66
|
+
const bottomBar = document.getElementById('bottomBar');
|
|
67
|
+
const bottomBarFill = document.getElementById('bottomBarFill');
|
|
68
|
+
const progressInfo = document.getElementById('progressInfo');
|
|
69
|
+
const progressLabelEl = document.getElementById('progressLabel');
|
|
70
|
+
const progressPercentEl = document.getElementById('progressPercent');
|
|
71
|
+
|
|
72
|
+
titleEl.textContent = title;
|
|
73
|
+
descEl.textContent = description;
|
|
74
|
+
|
|
75
|
+
root.dataset.variant = variant;
|
|
76
|
+
root.dataset.theme = themeResolved;
|
|
77
|
+
|
|
78
|
+
const hasImage = typeof image === 'string' && image.trim().length > 0;
|
|
79
|
+
if (hasImage) {
|
|
80
|
+
imgEl.src = image;
|
|
81
|
+
imgEl.style.display = 'block';
|
|
82
|
+
iconSvg.innerHTML = '';
|
|
83
|
+
} else {
|
|
84
|
+
imgEl.removeAttribute('src');
|
|
85
|
+
imgEl.style.display = 'none';
|
|
86
|
+
iconSvg.innerHTML = ICONS[variant] || ICONS.default;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setProgressUI(p) {
|
|
90
|
+
const pct = clampProgress(p);
|
|
91
|
+
progressLabelEl.textContent = progressLabel || '';
|
|
92
|
+
progressPercentEl.textContent = `${pct}%`;
|
|
93
|
+
bottomBarFill.style.transform = `scaleX(${pct / 100})`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let closing = false;
|
|
97
|
+
let autoTimer = null;
|
|
98
|
+
|
|
99
|
+
function enter() {
|
|
100
|
+
requestAnimationFrame(() => {
|
|
101
|
+
root.classList.remove('entering');
|
|
102
|
+
root.classList.add('entered');
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function requestClose() {
|
|
107
|
+
if (closing) return;
|
|
108
|
+
closing = true;
|
|
109
|
+
if (autoTimer) {
|
|
110
|
+
clearTimeout(autoTimer);
|
|
111
|
+
autoTimer = null;
|
|
112
|
+
}
|
|
113
|
+
root.classList.remove('entered');
|
|
114
|
+
root.classList.add('exiting');
|
|
115
|
+
setTimeout(() => window.electronNotify.sendClose(id), 220);
|
|
116
|
+
root.style.pointerEvents = 'none';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function startBottomBar() {
|
|
120
|
+
if (variant === 'loading') {
|
|
121
|
+
bottomBar.style.display = 'none';
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (variant === 'progress') {
|
|
126
|
+
progressInfo.style.display = 'flex';
|
|
127
|
+
setProgressUI(progress);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (duration <= 0) {
|
|
132
|
+
bottomBar.style.display = 'none';
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
bottomBar.style.display = 'block';
|
|
137
|
+
bottomBarFill.style.transform = 'scaleX(1)';
|
|
138
|
+
bottomBarFill.animate([{ transform: 'scaleX(1)' }, { transform: 'scaleX(0)' }], {
|
|
139
|
+
duration,
|
|
140
|
+
easing: 'linear',
|
|
141
|
+
fill: 'forwards'
|
|
142
|
+
});
|
|
143
|
+
autoTimer = setTimeout(() => requestClose(), duration);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
closeBtn.addEventListener('click', (e) => {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
requestClose();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
root.addEventListener('click', () => {
|
|
153
|
+
if (closing) return;
|
|
154
|
+
window.electronNotify.sendClick(id);
|
|
155
|
+
requestClose();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
root.addEventListener('keydown', (e) => {
|
|
159
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
root.click();
|
|
162
|
+
} else if (e.key === 'Escape') {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
requestClose();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
window.electronNotify.onReposition((_payload) => {
|
|
169
|
+
root.animate([{ transform: 'translateY(-2px)' }, { transform: 'translateY(0px)' }], {
|
|
170
|
+
duration: 240,
|
|
171
|
+
easing: 'ease-out'
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
window.electronNotify.onUpdate((payload) => {
|
|
176
|
+
if (payload.id !== id) return;
|
|
177
|
+
if (typeof payload.updates.description === 'string') descEl.textContent = payload.updates.description;
|
|
178
|
+
if (typeof payload.updates.loadingText === 'string') descEl.textContent = payload.updates.loadingText;
|
|
179
|
+
if (typeof payload.updates.progress === 'number') setProgressUI(payload.updates.progress);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
window.electronNotify.onTheme((payload) => {
|
|
183
|
+
root.dataset.theme = payload.theme;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
enter();
|
|
187
|
+
startBottomBar();
|
|
188
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function clampProgress(value: number): number {
|
|
2
|
+
if (Number.isNaN(value)) return 0;
|
|
3
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function setProgressFill(fillEl: HTMLElement, percent: number): void {
|
|
7
|
+
const p = clampProgress(percent);
|
|
8
|
+
fillEl.style.transform = `scaleX(${p / 100})`;
|
|
9
|
+
}
|
|
10
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
@keyframes spin {
|
|
2
|
+
from { transform: rotate(0deg); }
|
|
3
|
+
to { transform: rotate(360deg); }
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
@keyframes success-bounce {
|
|
7
|
+
0% { transform: translateX(var(--enter-x, 22px)) scale(0.8); }
|
|
8
|
+
60% { transform: translateX(0px) scale(1.05); }
|
|
9
|
+
100% { transform: translateX(0px) scale(1); }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@keyframes error-shake {
|
|
13
|
+
0% { transform: translateX(var(--enter-x, 22px)); }
|
|
14
|
+
40% { transform: translateX(-4px); }
|
|
15
|
+
70% { transform: translateX(4px); }
|
|
16
|
+
100% { transform: translateX(0px); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@keyframes warning-pulse {
|
|
20
|
+
0%, 100% { filter: saturate(100%); }
|
|
21
|
+
50% { filter: saturate(170%); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.spin {
|
|
25
|
+
animation: spin 900ms linear infinite;
|
|
26
|
+
transform-origin: 50% 50%;
|
|
27
|
+
}
|
|
28
|
+
|