dsh-desk-todo 0.0.0-stage → 2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-desk-todo contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,160 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # dsh-desk-todo · 桌面待办挂件
2
+
3
+ 把「**本周待办 + 长期任务**」贴在桌面右上角的一块小黑板上:无标题栏、不进任务栏、默认沉在窗口下面不挡东西;到点弹窗 + 叮一声。
4
+
5
+ ![本周视图](docs/screenshot-week.png)
6
+
7
+ - 左边:今天起 7 天的待办(没安排的周末也留一行,看得见周末)
8
+ - 右边:长期任务和 DDL 倒计时
9
+ - 单击任意一条 → 看/改备注、标记完成、删除
10
+ - 标题栏 **+** → 一句话快速新增(`15号下午3点在图书馆交作业` 这种)
11
+
12
+ ## 实现方式
13
+
14
+ 界面是 **Electron** 挂件(`app/`,无边框 + 透明 + 沉底),插件只负责找到一份 Electron 二进制并把它拉起来、崩了重启。
15
+
16
+ 为什么插件不自己开窗口:DSH 的插件宿主跑在 `ELECTRON_RUN_AS_NODE` 模式(`dsh-desktop-host`),进程里没有 `app` / `BrowserWindow`。
17
+
18
+ Electron 搜索顺序:
19
+
20
+ 1. 配置项 `electronPath`
21
+ 2. 环境变量 `DESH_DESK_TODO_ELECTRON`(见下方示例)
22
+ 3. `$DSH_HOME/data/surfcore/pet/runtime/electron-<ver>/electron.exe`(装了桌宠插件就有)
23
+ 4. `%LOCALAPPDATA%\Programs\electron\electron.exe`
24
+
25
+ 都找不到时**自动退回 WPF 版本**(`win/desk-todo.ps1`,Windows 自带,不需要 Electron,界面是同一套布局的简化实现)。
26
+
27
+ | 层 | 技术 | 说明 |
28
+ |---|---|---|
29
+ | 界面 | Electron(首选)/ WPF(退路) | 无边框透明窗口 |
30
+ | 提醒 | 自绘弹窗 + `SoundPlayer` / `Notification` | 不吃系统通知权限 |
31
+ | 一句话识别 | Node 正则解析器(`lib/parse.js`) | 通过 `127.0.0.1` 临时端口给界面调用 |
32
+ | 数据 | `$DSH_HOME/desk-todo/data.json` | 纯文本,改完自动刷新 |
33
+
34
+ ## 安装
35
+
36
+ ```bash
37
+ dsh plugin --profile desktop add dsh-desk-todo
38
+ # 或者
39
+ dsh plugin --profile desktop add github:Astryve/dsh-desk-todo
40
+ ```
41
+
42
+ 装完**重启 DSH**,挂件会出现在屏幕右上角。
43
+
44
+ 本地开发:
45
+
46
+ ```bash
47
+ git clone https://github.com/Astryve/dsh-desk-todo
48
+ dsh plugin --profile desktop add link:/绝对路径/dsh-desk-todo
49
+ ```
50
+
51
+ ## 第一次打开
52
+
53
+ - 挂件是空的(只有 7 天日期 + 空长期区)
54
+ - 数据文件被创建在 `$DSH_HOME/desk-todo/data.json`(Windows 默认 `C:\Users\<你>\.dsh\desk-todo\data.json`;本机把 DSH 数据放 D 盘时就是 `D:\DSH\data\desk-todo\`)
55
+ - 想立刻看到效果,把仓库里的 `data/example.json` 复制成 `data.json` 即可
56
+
57
+ ## 怎么加待办
58
+
59
+ ### 一句话识别(推荐)
60
+
61
+ 点 **+**,输入:
62
+
63
+ ```
64
+ 15号下午3点在图书馆交作业
65
+ ```
66
+
67
+ 会自动拆成 `2026-11-15 · 15:00–16:00 · 图书馆 · 交作业`。识别服务由插件提供;插件没起来时退化为「今天 09:00 + 原文标题」。
68
+
69
+ 支持:
70
+
71
+ | 类型 | 例子 |
72
+ |---|---|
73
+ | 日期 | `15号` `11月20日` `2026-11-20` `今天/明天/后天` `周五` `下周一` |
74
+ | 时间 | `3点` `7点半` `十点` `9点到11点`,可配 `上午/下午/晚上/中午` |
75
+ | 地点 | `在图书馆` `地点:主楼W3阶`,也认 `东区414` `W3阶` `宿舍楼下` |
76
+ | 标题 | 自动去掉时间地点,只留动作 |
77
+
78
+ ### 改 JSON
79
+
80
+ `data.json` 是普通 JSON,改完存盘挂件自动刷新。字段见下。
81
+
82
+ ## 提醒规则
83
+
84
+ | 时机 | 表现 |
85
+ |---|---|
86
+ | 提前 N 分钟(默认 15,可设 0) | 右下角弹窗 + 轻提示音 |
87
+ | 到点 | 弹窗 + 更响提示音 |
88
+ | 逾期未完成 | 每 10 分钟一次,最多 3 次 |
89
+ | 长期任务临期 | 剩 3 天 / 1 天 / 当天各一次 |
90
+
91
+ ## 鼠标操作
92
+
93
+ | 操作 | 效果 |
94
+ |---|---|
95
+ | 拖标题栏 | 移动(位置自动记住) |
96
+ | 点 + | 快速新增 |
97
+ | 点 - | 隐藏(插件仍在跑,重启 DSH 会回来) |
98
+ | 点条目 | 详情:备注、完成、删除 |
99
+
100
+ ## 数据格式
101
+
102
+ ```json
103
+ {
104
+ "semester": { "name": "2026-2027 秋", "week1Monday": "2026-08-31" },
105
+ "events": [
106
+ { "id": "e1", "title": "数值分析", "day": "2026-11-12",
107
+ "start": "13:30", "end": "16:40", "place": "武进阶教楼W3阶",
108
+ "color": "gold", "remindMin": 15, "repeat": "weekly", "note": "带作业" }
109
+ ],
110
+ "longterm": [
111
+ { "id": "l1", "title": "毕业设计开题", "due": "2026-12-20", "done": false, "note": "" }
112
+ ],
113
+ "settings": { "w": 520, "h": 330, "x": null, "y": 16, "opacity": 0.85 }
114
+ }
115
+ ```
116
+
117
+ | 字段 | 说明 |
118
+ |---|---|
119
+ | `day` | `YYYY-MM-DD`;`repeat: "weekly"` 时它决定周几 |
120
+ | `start` / `end` | `HH:MM` 24 小时制 |
121
+ | `place` | 自动简写:`武进东区国际教室414` → `东区414` |
122
+ | `color` | 色条:`gold` `green` `blue` `pink` `orange` |
123
+ | `remindMin` | 提前几分钟;`0` = 只到点提醒 |
124
+ | `doneOn` | 某天标记完成的日子(每周重复的第二天自动恢复) |
125
+ | `semester.week1Monday` | 有它标题就显示「第 N 周」 |
126
+
127
+ ## 配置
128
+
129
+ ```yaml
130
+ # ~/.dsh/profiles/<profile>/cordis.patch.yml
131
+ - id: desk-todo
132
+ config:
133
+ alwaysOnTop: true # 想让它盖在窗口上面就打开(默认 false = 沉底)
134
+ silent: true # 只弹窗不出声
135
+ width: 520
136
+ height: 330
137
+ top: 16
138
+ right: 16
139
+ ```
140
+
141
+ ## 常见问题
142
+
143
+ **装完没看到?**
144
+ ① 重启 DSH 了吗;② 挂件默认沉在最底层,最小化其它窗口才看得到;③ 打开 `%TEMP%\desk-todo-load.txt`,没有 `spawned powershell pid=...` 就是插件没被加载。
145
+
146
+ **弹窗不响?**
147
+ 配置里 `silent: true` 关了声音,或系统音量/输出设备不对。
148
+
149
+ **能改窗口大小吗?**
150
+ 改 `data.json` 里 `settings.w/h`,或配置里 `width/height`(只影响首次创建)。
151
+
152
+ **数据会不会丢?**
153
+ 不会,全在 `data.json`;卸载插件不删数据。
154
+
155
+ **非 Windows?**
156
+ WPF 只有 Windows 有,这个版本不支持 macOS / Linux。
157
+
158
+ ## License
159
+
160
+ MIT
package/app/main.js ADDED
@@ -0,0 +1,244 @@
1
+ const { app, BrowserWindow, Tray, Menu, ipcMain, Notification, nativeImage, screen } = require('electron');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const DATA_FILE = process.env.DESK_TODO_DATA || path.join(__dirname, 'data.json');
6
+ const CHIME = path.join(__dirname, 'assets', 'chime.wav');
7
+ const ICON = path.join(__dirname, 'assets', 'icon.png');
8
+
9
+ app.setPath('userData', path.join(__dirname, '.cache'));
10
+ if (process.platform === 'win32') app.setAppUserModelId('com.dsh.desktopwidget');
11
+
12
+ let win = null;
13
+ let quitting = false;
14
+ let tray = null;
15
+ let data = null;
16
+ const fired = new Map(); // key -> timestamp
17
+
18
+ function readData() {
19
+ try { return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); }
20
+ catch { return { events: [], longterm: [], settings: {} }; }
21
+ }
22
+ function writeData(d) {
23
+ try { fs.writeFileSync(DATA_FILE, JSON.stringify(d, null, 2)); data = d; } catch (e) { console.error(e); }
24
+ }
25
+
26
+ function clampSize(s) {
27
+ const w = Math.max(300, Math.min(960, Math.round(Number(s && s.w) || 420)));
28
+ const h = Math.max(140, Math.min(680, Math.round(Number(s && s.h) || 260)));
29
+ return { w, h };
30
+ }
31
+
32
+ function applyBounds(s) {
33
+ if (!win) return;
34
+ const { w, h } = clampSize(s);
35
+ const area = screen.getPrimaryDisplay().workArea;
36
+ let x = Number.isFinite(s && s.x) ? Math.round(s.x) : area.x + area.width - w - 16;
37
+ let y = Number.isFinite(s && s.y) ? Math.round(s.y) : area.y + 16;
38
+ x = Math.max(area.x, Math.min(area.x + area.width - w, x));
39
+ y = Math.max(area.y, Math.min(area.y + area.height - h, y));
40
+ const b = win.getBounds();
41
+ if (b.width !== w || b.height !== h || b.x !== x || b.y !== y) win.setBounds({ x, y, width: w, height: h });
42
+ }
43
+
44
+ function saveGeometry() {
45
+ if (!win || !data) return;
46
+ const b = win.getBounds();
47
+ data.settings = Object.assign({}, data.settings, { w: b.width, h: b.height, x: b.x, y: b.y });
48
+ writeData(data);
49
+ }
50
+
51
+ function createWindow() {
52
+ const s = (data && data.settings) || {};
53
+ const { w, h } = clampSize(s);
54
+ const area = screen.getPrimaryDisplay().workArea;
55
+ const x = Number.isFinite(s.x) ? Math.round(s.x) : area.x + area.width - w - 16;
56
+ const y = Number.isFinite(s.y) ? Math.round(s.y) : area.y + 16;
57
+
58
+ win = new BrowserWindow({
59
+ width: w, height: h, x, y,
60
+ frame: false, transparent: true, resizable: true, movable: true,
61
+ alwaysOnTop: false, skipTaskbar: true, hasShadow: false,
62
+ minimizable: false, maximizable: false, fullscreenable: false,
63
+ webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false }
64
+ });
65
+ win.setVisibleOnAllWorkspaces(true);
66
+ win.showInactive();
67
+ // 沉到最底层:不抢焦点、不遮挡其它窗口
68
+ win.on('blur', () => { try { win.setAlwaysOnTop(false); } catch (e) {} });
69
+ // Win+D / 显示桌面 会最小化所有窗口;立刻恢复,让挂件重新贴回桌面
70
+ win.on('minimize', (e) => { try { e.preventDefault(); win.restore(); win.showInactive(); } catch (err) {} });
71
+ win.loadFile(path.join(__dirname, 'ui', 'index.html'));
72
+ win.on('moved', () => { clearTimeout(win._t); win._t = setTimeout(saveGeometry, 400); });
73
+ win.on('resized', () => { clearTimeout(win._t); win._t = setTimeout(saveGeometry, 400); });
74
+ win.on('closed', () => { win = null; if (!quitting) recreateSoon(800); });
75
+ }
76
+
77
+ function toggleShow() {
78
+ if (!win) return createWindow();
79
+ if (win.isVisible()) win.hide(); else win.show();
80
+ }
81
+
82
+ function buildTray() {
83
+ const img = nativeImage.createFromPath(ICON);
84
+ tray = new Tray(img.isEmpty() ? nativeImage.createEmpty() : img);
85
+ tray.setToolTip('桌面挂件');
86
+ const menu = Menu.buildFromTemplate([
87
+ { label: '显示 / 隐藏', click: toggleShow },
88
+ { label: '回到右上角', click: () => { if (!win) return; data.settings.x = null; data.settings.y = 16; writeData(data); applyBounds(data.settings); } },
89
+ { type: 'separator' },
90
+ { label: '测试提醒', click: () => { popupToast('数值分析', '15 分钟后开始 · 阶教楼W3阶', false); if (win && !win.isDestroyed()) win.webContents.send('flash', { key: '__test__', urgent: false }); } },
91
+ { label: '打开数据文件', click: () => { require('electron').shell.openPath(DATA_FILE); } },
92
+ { label: '重新载入数据', click: () => { data = readData(); sendState(); } },
93
+ { type: 'separator' },
94
+ { label: '退出', click: () => app.quit() }
95
+ ]);
96
+ tray.setContextMenu(menu);
97
+ tray.on('click', toggleShow);
98
+ }
99
+
100
+ function sendState() {
101
+ if (win && !win.isDestroyed()) win.webContents.send('state', data);
102
+ }
103
+
104
+ /* ───────── 自定义弹窗提醒(不依赖系统通知权限) ───────── */
105
+ let toastWin = null;
106
+ function popupToast(title, body, urgent) {
107
+ try {
108
+ if (toastWin && !toastWin.isDestroyed()) toastWin.close();
109
+ const area = screen.getPrimaryDisplay().workArea;
110
+ const w = 380, h = 96;
111
+ toastWin = new BrowserWindow({
112
+ width: w, height: h,
113
+ x: area.x + area.width - w - 16,
114
+ y: area.y + area.height - h - 72,
115
+ frame: false, transparent: true, resizable: false, movable: false,
116
+ alwaysOnTop: false, skipTaskbar: true, hasShadow: false, focusable: false,
117
+ webPreferences: { contextIsolation: true, nodeIntegration: false }
118
+ });
119
+ toastWin.setAlwaysOnTop(true, 'screen-saver');
120
+ const qs = '?t=' + encodeURIComponent(title) + '&b=' + encodeURIComponent(body) + '&u=' + (urgent ? 1 : 0) + '&ms=' + (urgent ? 12000 : 8000);
121
+ toastWin.loadURL('file://' + path.join(__dirname, 'ui', 'toast.html') + qs);
122
+ toastWin.on('closed', () => { toastWin = null; });
123
+ } catch (e) { console.error('popup fail', e); }
124
+ }
125
+
126
+ /* ───────── 提醒调度 ───────── */
127
+ function pad(n) { return String(n).padStart(2, '0'); }
128
+ function todayKey(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); }
129
+ function minutesOf(hhmm) { const [h, m] = String(hhmm || '00:00').split(':').map(Number); return (h || 0) * 60 + (m || 0); }
130
+
131
+ function eventOccursOn(ev, date) {
132
+ const t = todayKey(date);
133
+ if (ev.day === t) return true;
134
+ if (ev.repeat === 'weekly') {
135
+ const base = new Date(ev.day + 'T00:00:00');
136
+ return base.getDay() === date.getDay() && date >= new Date(ev.day + 'T00:00:00');
137
+ }
138
+ return false;
139
+ }
140
+
141
+ function fire(title, body, key, urgent) {
142
+ if (fired.has(key)) return;
143
+ fired.set(key, Date.now());
144
+ console.log('[notify]', title, '|', body);
145
+ try {
146
+ const n = new Notification({ title, body, icon: ICON, silent: !!(data && data.settings && data.settings.silent) });
147
+ n.on('click', () => { if (win) { win.show(); win.focus(); } });
148
+ n.show();
149
+ } catch (e) { console.error('notify fail', e); }
150
+ popupToast(title, body, urgent);
151
+ if (win && !win.isDestroyed()) {
152
+ win.webContents.send('flash', { key, urgent: !!urgent });
153
+ if (!(data && data.settings && data.settings.silent)) win.flashFrame(true);
154
+ }
155
+ }
156
+
157
+ function tick() {
158
+ if (!data) return;
159
+ const now = new Date();
160
+ const nowMin = now.getHours() * 60 + now.getMinutes();
161
+ const t = todayKey(now);
162
+
163
+ for (const ev of data.events || []) {
164
+ if (!eventOccursOn(ev, now)) continue;
165
+ if (ev.doneOn === t) continue;
166
+ const startMin = minutesOf(ev.start);
167
+ const remind = Number.isFinite(Number(ev.remindMin)) ? Number(ev.remindMin) : 15;
168
+ const diff = startMin - nowMin;
169
+ if (remind > 0 && diff <= remind && diff > remind - 2) {
170
+ fire('马上要上课 · ' + ev.title, (remind + ' 分钟后开始') + (ev.place ? ' · ' + ev.place : ''), ev.id + '|' + t + '|pre');
171
+ }
172
+ if (diff <= 0 && diff > -2) {
173
+ fire('开始了 · ' + ev.title, (ev.place || '') + (ev.who ? ' · ' + ev.who : ''), ev.id + '|' + t + '|now', true);
174
+ }
175
+ if (diff < -2 && diff > -90) {
176
+ const stage = Math.floor((-diff) / 10);
177
+ if (stage >= 1 && stage <= 3) {
178
+ fire('还没完成 · ' + ev.title, '已经过去 ' + (-diff) + ' 分钟', ev.id + '|' + t + '|late' + stage, true);
179
+ }
180
+ }
181
+ }
182
+
183
+ for (const it of data.longterm || []) {
184
+ if (it.done || !it.due) continue;
185
+ const due = new Date(it.due + 'T23:59:00');
186
+ const days = Math.ceil((due - now) / 86400000);
187
+ if (days === 3 || days === 1 || days === 0) {
188
+ fire('长期任务临期 · ' + it.title, days === 0 ? '今天到期' : days + ' 天后到期(' + it.due + ')', it.id + '|ddl|' + days);
189
+ }
190
+ }
191
+ }
192
+
193
+ /* ───────── IPC ───────── */
194
+ ipcMain.handle('get-state', () => data);
195
+ ipcMain.handle('save-data', (_e, next) => { writeData(next); sendState(); return true; });
196
+ ipcMain.on('set-ignore', (_e, ignore) => {
197
+ if (win && !win.isDestroyed()) win.setIgnoreMouseEvents(!!ignore, { forward: true });
198
+ });
199
+ ipcMain.on('set-bounds', (_e, s) => { data.settings = Object.assign({}, data.settings, s); writeData(data); applyBounds(data.settings); });
200
+ ipcMain.on('set-opacity', (_e, v) => { data.settings.opacity = Math.max(0.25, Math.min(0.95, Number(v) || 0.75)); writeData(data); sendState(); });
201
+ ipcMain.on('hide-widget', () => { if (win) win.hide(); });
202
+ ipcMain.on('open-data', () => { require('electron').shell.openPath(DATA_FILE); });
203
+ ipcMain.on('reload', () => { data = readData(); sendState(); });
204
+
205
+ // 单实例:避免多开导致"旧窗口还挡着鼠标"的幽灵问题
206
+ const gotLock = app.requestSingleInstanceLock();
207
+ if (!gotLock) {
208
+ app.quit();
209
+ } else {
210
+ app.on('second-instance', () => {
211
+ try { if (win) { win.showInactive(); } } catch (e) {}
212
+ });
213
+ }
214
+
215
+ app.whenReady().then(() => {
216
+ if (!fs.existsSync(DATA_FILE)) writeData({ events: [], longterm: [], settings: {} });
217
+ data = readData();
218
+ createWindow();
219
+ buildTray();
220
+ // Win+D / 显示桌面 会把窗口最小化;轮询拉回来,保持"桌面贴纸"的效果
221
+ setInterval(() => {
222
+ try { if (win && !win.isDestroyed() && win.isMinimized()) { win.restore(); win.showInactive(); } } catch (e) {}
223
+ }, 1200);
224
+ setInterval(tick, 20000);
225
+ tick();
226
+ fs.watchFile(DATA_FILE, { interval: 1500 }, () => { data = readData(); sendState(); });
227
+ });
228
+
229
+ app.on('before-quit', () => { quitting = true; });
230
+ app.on('window-all-closed', () => { if (quitting) app.quit(); });
231
+
232
+ // 桌面右键刷新 / 资源管理器重启会把窗口顶掉:自动重建,保持长期显示
233
+ let recreateTimer = null;
234
+ function recreateSoon(ms) {
235
+ if (quitting) return;
236
+ clearTimeout(recreateTimer);
237
+ recreateTimer = setTimeout(() => {
238
+ if (quitting) return;
239
+ if (!win || win.isDestroyed()) { try { createWindow(); } catch (e) { } }
240
+ }, ms || 800);
241
+ }
242
+ app.on('browser-window-created', (_e, w) => {
243
+ w.webContents.on('render-process-gone', () => recreateSoon(500));
244
+ });
package/app/preload.js ADDED
@@ -0,0 +1,13 @@
1
+ const { contextBridge, ipcRenderer } = require('electron');
2
+ contextBridge.exposeInMainWorld('widget', {
3
+ getState: () => ipcRenderer.invoke('get-state'),
4
+ saveData: (d) => ipcRenderer.invoke('save-data', d),
5
+ setIgnore: (v) => ipcRenderer.send('set-ignore', v),
6
+ setBounds: (s) => ipcRenderer.send('set-bounds', s),
7
+ setOpacity: (v) => ipcRenderer.send('set-opacity', v),
8
+ hide: () => ipcRenderer.send('hide-widget'),
9
+ openData: () => ipcRenderer.send('open-data'),
10
+ reload: () => ipcRenderer.send('reload'),
11
+ onState: (cb) => ipcRenderer.on('state', (_e, d) => cb(d)),
12
+ onFlash: (cb) => ipcRenderer.on('flash', (_e, p) => cb(p))
13
+ });
@@ -0,0 +1,126 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>桌面挂件</title>
6
+ <link rel="stylesheet" href="style.css" />
7
+ </head>
8
+ <body class="mode-m">
9
+ <div id="app">
10
+ <header class="bar" data-hit>
11
+ <div class="title" id="weekLabel">本周</div>
12
+ <div class="tools">
13
+ <button id="btnAdd" class="chip" title="新增待办(右键=新增长期任务)">+</button>
14
+ <button id="btnView" class="chip" title="切换 本周 / 长期">长期</button>
15
+ <button id="btnCfg" class="chip" title="设置">⚙</button>
16
+ <button id="btnHide" class="chip" title="隐藏">-</button>
17
+ </div>
18
+ </header>
19
+
20
+ <main class="panes">
21
+ <section id="paneWeek" class="pane">
22
+ <div id="weekList" class="list"></div>
23
+ </section>
24
+ <div class="divider"></div>
25
+ <section id="paneLong" class="pane">
26
+ <div id="longList" class="list"></div>
27
+ </section>
28
+ </main>
29
+ </div>
30
+
31
+ <div id="detail" class="overlay hidden" data-hit>
32
+ <div class="card">
33
+ <div class="card-top">
34
+ <span id="dTitle" class="card-title"></span>
35
+ <button id="dClose" class="x">×</button>
36
+ </div>
37
+ <div id="dMeta" class="meta"></div>
38
+ <label class="lbl">我的备注</label>
39
+ <textarea id="dNote" rows="3" placeholder="写点备注…(自动保存)"></textarea>
40
+ <div class="card-actions">
41
+ <button id="dDone" class="btn">标记完成</button>
42
+ <button id="dEdit" class="btn ghost">编辑</button>
43
+ <button id="dDel" class="btn ghost">删除</button>
44
+ <button id="dOpenData" class="btn ghost">数据</button>
45
+ </div>
46
+ </div>
47
+ </div>
48
+
49
+ <div id="editor" class="overlay hidden" data-hit>
50
+ <div class="card">
51
+ <div class="card-top">
52
+ <span id="eKind" class="card-title">新增待办</span>
53
+ <button id="eCancel2" class="x">×</button>
54
+ </div>
55
+ <label class="lbl">标题</label>
56
+ <input id="eTitle" type="text" placeholder="例如 交作业 / 数值分析" />
57
+ <div class="only-event">
58
+ <div class="row2">
59
+ <label class="lbl">日期 <input id="eDate" type="date" /></label>
60
+ <label class="lbl">开始
61
+ <span class="timebox">
62
+ <button type="button" id="eStartH" class="tm" data-unit="hour">08</button><span class="colon">:</span><button type="button" id="eStartM" class="tm" data-unit="min">00</button>
63
+ </span>
64
+ </label>
65
+ <label class="lbl">结束
66
+ <span class="timebox">
67
+ <button type="button" id="eEndH" class="tm" data-unit="hour">09</button><span class="colon">:</span><button type="button" id="eEndM" class="tm" data-unit="min">35</button>
68
+ </span>
69
+ </label>
70
+ </div>
71
+ <div class="row2">
72
+ <label class="lbl">地点 <input id="ePlace" type="text" placeholder="东区414" /></label>
73
+ <label class="lbl">提前(分钟) <input id="eRemind" type="number" min="0" max="180" step="5" /></label>
74
+ <label class="lbl">重复
75
+ <select id="eRepeat">
76
+ <option value="none">只一次</option>
77
+ <option value="weekly">每周</option>
78
+ </select>
79
+ </label>
80
+ </div>
81
+ </div>
82
+ <div class="only-long">
83
+ <label class="lbl">截止日期 <input id="eDue" type="date" /></label>
84
+ </div>
85
+ <label class="lbl">备注(可粘贴「几号几点在哪做什么」,点右侧识别)</label>
86
+ <textarea id="eNote" rows="2"></textarea>
87
+ <div class="parse-row">
88
+ <span id="eHint" class="hint"></span>
89
+ <button id="eParse" class="btn ghost small" title="从备注里识别时间地点并填入">识别</button>
90
+ </div>
91
+ <div class="card-actions">
92
+ <button id="eSave" class="btn">保存</button>
93
+ <button id="eCancel" class="btn ghost">取消</button>
94
+ <button id="eDelete" class="btn ghost">删除</button>
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <div id="cfg" class="overlay hidden" data-hit>
100
+ <div class="card">
101
+ <div class="card-top">
102
+ <span class="card-title">设置</span>
103
+ <button id="cClose" class="x">×</button>
104
+ </div>
105
+ <label class="lbl">尺寸预设</label>
106
+ <div class="presets">
107
+ <button class="btn ghost" data-preset="400x220">400×220</button>
108
+ <button class="btn ghost" data-preset="520x330">520×330</button>
109
+ <button class="btn ghost" data-preset="640x400">640×400</button>
110
+ <button class="btn ghost" data-preset="760x460">760×460</button>
111
+ </div>
112
+ <div class="row2">
113
+ <label class="lbl">宽 <input id="cW" type="number" min="300" max="960" step="10"></label>
114
+ <label class="lbl">高 <input id="cH" type="number" min="140" max="680" step="10"></label>
115
+ </div>
116
+ <label class="lbl">背景不透明度 <span id="cOpV"></span></label>
117
+ <input id="cOpacity" type="range" min="0.3" max="0.95" step="0.01">
118
+ <div class="card-actions">
119
+ <button id="cApply" class="btn">应用</button>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <script src="renderer.js"></script>
125
+ </body>
126
+ </html>