mingyang22 commited on
Commit
c433ecb
·
verified ·
1 Parent(s): a7c6cce

Add PWA support and reading preview panel

Browse files
Files changed (1) hide show
  1. app.py +162 -41
app.py CHANGED
@@ -7,12 +7,104 @@ from huggingface_hub import HfApi, hf_hub_download
7
  from datetime import datetime
8
  import shutil
9
  from pathlib import Path
 
10
 
11
  # --- 配置 (优先从环境变量读取) ---
12
  DATASET_REPO_ID = os.environ.get("DATASET_REPO_ID", "mingyang22/huggingface-notes")
13
  HF_TOKEN = os.environ.get("HF_TOKEN") # 必须在 Space 设置中配置
14
  REMOTE_NOTES_PATH = "db/notes.json"
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  def get_default_data_dir():
17
  custom_dir = os.environ.get("HF_NOTES_DATA_DIR")
18
  if custom_dir:
@@ -212,16 +304,23 @@ def delete_note(note_id):
212
  write_notes(notes)
213
  backup_msg = sync_manager.push()
214
  return f"🗑️ 笔记已删除 | {backup_msg}", load_notes_list()
215
-
216
- # --- Gradio UI 界面 ---
217
- with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as demo:
218
- gr.Markdown("# 📓 Hugging Face 个人笔记云端版")
219
- gr.Markdown("实时同步本地 Quicker 动作数据。数据由私有 Dataset 承载,安全、持久、版本可追溯。")
220
-
221
- with gr.Row():
222
- with gr.Column(scale=1):
223
- note_list = gr.Dataframe(
224
- headers=["ID", "标题", "最后修改"],
 
 
 
 
 
 
 
225
  datatype=["str", "str", "str"],
226
  value=load_notes_list(),
227
  interactive=False,
@@ -231,36 +330,58 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as demo:
231
  status_output = gr.Markdown("系统就绪")
232
 
233
  with gr.Column(scale=2):
234
- with gr.Group():
235
- target_id = gr.Textbox(visible=False)
236
- in_title = gr.Textbox(label="标题", placeholder="输入笔记标题...")
237
- in_content = gr.TextArea(label="正文内容", lines=15, placeholder="记录您的想法...")
238
-
239
- with gr.Row():
240
- btn_save = gr.Button("💾 保存并推送到云端", variant="primary")
241
- btn_new = gr.Button(" 新建笔记")
242
- btn_del = gr.Button("🗑️ 删除笔记", variant="stop")
 
243
 
244
  # 事件绑定
245
- def on_select(evt: gr.SelectData):
246
- # evt.index[0] 是行号
247
- df = load_notes_list()
248
- selected_id = df[evt.index[0]][0]
249
- title, content = get_note_content(selected_id)
250
- return selected_id, title, content
251
-
252
- note_list.select(on_select, None, [target_id, in_title, in_content])
253
-
254
- btn_save.click(save_note, [target_id, in_title, in_content], [status_output, note_list])
255
-
256
- btn_new.click(lambda: (None, "新笔记", ""), None, [target_id, in_title, in_content])
257
-
258
- btn_del.click(delete_note, [target_id], [status_output, note_list])
259
-
260
- btn_refresh.click(lambda: (sync_manager.pull(), load_notes_list()), None, [status_output, note_list])
261
-
262
- # 启动时自动从云端拉取
263
- demo.load(lambda: (sync_manager.pull(), load_notes_list()), None, [status_output, note_list])
264
-
265
- if __name__ == "__main__":
266
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  from datetime import datetime
8
  import shutil
9
  from pathlib import Path
10
+ from fastapi.responses import Response
11
 
12
  # --- 配置 (优先从环境变量读取) ---
13
  DATASET_REPO_ID = os.environ.get("DATASET_REPO_ID", "mingyang22/huggingface-notes")
14
  HF_TOKEN = os.environ.get("HF_TOKEN") # 必须在 Space 设置中配置
15
  REMOTE_NOTES_PATH = "db/notes.json"
16
 
17
+ APP_MANIFEST = {
18
+ "name": "HF 笔记",
19
+ "short_name": "HF笔记",
20
+ "start_url": "/",
21
+ "display": "standalone",
22
+ "background_color": "#0f172a",
23
+ "theme_color": "#2563eb",
24
+ "description": "Hugging Face 云端同步笔记(PWA)",
25
+ "icons": [
26
+ {
27
+ "src": "/pwa-icon.svg",
28
+ "sizes": "any",
29
+ "type": "image/svg+xml",
30
+ "purpose": "any maskable"
31
+ }
32
+ ],
33
+ }
34
+
35
+ PWA_ICON_SVG = """<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'>
36
+ <rect width='128' height='128' rx='24' fill='#1d4ed8'/>
37
+ <path d='M34 28h60a6 6 0 0 1 6 6v60a6 6 0 0 1-6 6H34a6 6 0 0 1-6-6V34a6 6 0 0 1 6-6z' fill='#fff' opacity='0.95'/>
38
+ <path d='M44 48h40M44 64h40M44 80h26' stroke='#1d4ed8' stroke-width='6' stroke-linecap='round'/>
39
+ </svg>"""
40
+
41
+ PWA_SW_JS = """
42
+ const CACHE_NAME = 'hf-notes-pwa-v1';
43
+ const CORE_ASSETS = ['/', '/manifest.webmanifest', '/pwa-icon.svg'];
44
+ self.addEventListener('install', (event) => {
45
+ event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(CORE_ASSETS)));
46
+ self.skipWaiting();
47
+ });
48
+ self.addEventListener('activate', (event) => {
49
+ event.waitUntil(
50
+ caches.keys().then((keys) =>
51
+ Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
52
+ )
53
+ );
54
+ self.clients.claim();
55
+ });
56
+ self.addEventListener('fetch', (event) => {
57
+ const req = event.request;
58
+ if (req.method !== 'GET') return;
59
+ const url = new URL(req.url);
60
+ if (url.origin !== self.location.origin) return;
61
+ if (req.mode === 'navigate') {
62
+ event.respondWith(
63
+ fetch(req).then((res) => {
64
+ const copy = res.clone();
65
+ caches.open(CACHE_NAME).then((cache) => cache.put(req, copy));
66
+ return res;
67
+ }).catch(() => caches.match(req).then((res) => res || caches.match('/')))
68
+ );
69
+ return;
70
+ }
71
+ event.respondWith(caches.match(req).then((cached) => cached || fetch(req)));
72
+ });
73
+ """
74
+
75
+ PWA_HEAD = """
76
+ <link rel="manifest" href="/manifest.webmanifest" />
77
+ <meta name="theme-color" content="#2563eb" />
78
+ <script>
79
+ (() => {
80
+ if ('serviceWorker' in navigator) {
81
+ window.addEventListener('load', () => navigator.serviceWorker.register('/sw.js').catch(() => {}));
82
+ }
83
+ const DRAFT_KEY = 'hf_notes_web_draft_v1';
84
+ function readDraft() {
85
+ try { return JSON.parse(localStorage.getItem(DRAFT_KEY) || '{}'); } catch (_) { return {}; }
86
+ }
87
+ function writeDraft(data) {
88
+ try { localStorage.setItem(DRAFT_KEY, JSON.stringify(data || {})); } catch (_) {}
89
+ }
90
+ function bindDraftPersistence() {
91
+ const titleInput = document.querySelector('#note_title textarea, #note_title input');
92
+ const contentInput = document.querySelector('#note_content textarea');
93
+ const draft = readDraft();
94
+ if (titleInput && !titleInput.value && draft.title) titleInput.value = draft.title;
95
+ if (contentInput && !contentInput.value && draft.content) contentInput.value = draft.content;
96
+ if (titleInput) {
97
+ titleInput.addEventListener('input', () => writeDraft({ title: titleInput.value, content: contentInput ? contentInput.value : '' }));
98
+ }
99
+ if (contentInput) {
100
+ contentInput.addEventListener('input', () => writeDraft({ title: titleInput ? titleInput.value : '', content: contentInput.value }));
101
+ }
102
+ }
103
+ window.addEventListener('load', () => setTimeout(bindDraftPersistence, 800));
104
+ })();
105
+ </script>
106
+ """
107
+
108
  def get_default_data_dir():
109
  custom_dir = os.environ.get("HF_NOTES_DATA_DIR")
110
  if custom_dir:
 
304
  write_notes(notes)
305
  backup_msg = sync_manager.push()
306
  return f"🗑️ 笔记已删除 | {backup_msg}", load_notes_list()
307
+
308
+ def render_preview_md(title, content):
309
+ t = (title or "").strip()
310
+ c = content or ""
311
+ if not t and not c:
312
+ return "## 预览\n\n暂无内容。"
313
+ return f"## {t or '未命名笔记'}\n\n{c}"
314
+
315
+ # --- Gradio UI 界面 ---
316
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), head=PWA_HEAD) as demo:
317
+ gr.Markdown("# 📓 Hugging Face 个人笔记云端版")
318
+ gr.Markdown("实时同步本地 Quicker 动作数据。支持 PWA 安装与离线壳缓存,方便长期查看与编辑。")
319
+
320
+ with gr.Row():
321
+ with gr.Column(scale=1):
322
+ note_list = gr.Dataframe(
323
+ headers=["ID", "标题", "最后修改"],
324
  datatype=["str", "str", "str"],
325
  value=load_notes_list(),
326
  interactive=False,
 
330
  status_output = gr.Markdown("系统就绪")
331
 
332
  with gr.Column(scale=2):
333
+ with gr.Group():
334
+ target_id = gr.Textbox(visible=False)
335
+ in_title = gr.Textbox(label="标题", placeholder="输入笔记标题...", elem_id="note_title")
336
+ in_content = gr.TextArea(label="正文内容", lines=15, placeholder="记录您的想法...", elem_id="note_content")
337
+ preview = gr.Markdown(value="## 预览\n\n暂无内容。", label="阅读预览")
338
+
339
+ with gr.Row():
340
+ btn_save = gr.Button("💾 保存并推送到云端", variant="primary")
341
+ btn_new = gr.Button(" 新建笔记")
342
+ btn_del = gr.Button("🗑️ 删除笔记", variant="stop")
343
 
344
  # 事件绑定
345
+ def on_select(evt: gr.SelectData):
346
+ # evt.index[0] 是行号
347
+ df = load_notes_list()
348
+ selected_id = df[evt.index[0]][0]
349
+ title, content = get_note_content(selected_id)
350
+ return selected_id, title, content, render_preview_md(title, content)
351
+
352
+ note_list.select(on_select, None, [target_id, in_title, in_content, preview])
353
+
354
+ btn_save.click(save_note, [target_id, in_title, in_content], [status_output, note_list]).then(
355
+ render_preview_md, [in_title, in_content], [preview]
356
+ )
357
+
358
+ btn_new.click(lambda: (None, "新笔记", "", render_preview_md("新笔记", "")), None, [target_id, in_title, in_content, preview])
359
+
360
+ btn_del.click(delete_note, [target_id], [status_output, note_list]).then(
361
+ lambda: ("", "", "## 预览\n\n暂无内容。"), None, [in_title, in_content, preview]
362
+ )
363
+
364
+ btn_refresh.click(lambda: (sync_manager.pull(), load_notes_list()), None, [status_output, note_list])
365
+ in_content.change(render_preview_md, [in_title, in_content], [preview], queue=False)
366
+ in_title.change(render_preview_md, [in_title, in_content], [preview], queue=False)
367
+
368
+ # 启动时自动从云端拉取
369
+ demo.load(lambda: (sync_manager.pull(), load_notes_list(), "## 预览\n\n暂无内容。"), None, [status_output, note_list, preview])
370
+
371
+ @demo.app.get("/manifest.webmanifest")
372
+ def pwa_manifest():
373
+ return Response(
374
+ content=json.dumps(APP_MANIFEST, ensure_ascii=False),
375
+ media_type="application/manifest+json"
376
+ )
377
+
378
+ @demo.app.get("/sw.js")
379
+ def pwa_service_worker():
380
+ return Response(content=PWA_SW_JS, media_type="application/javascript")
381
+
382
+ @demo.app.get("/pwa-icon.svg")
383
+ def pwa_icon():
384
+ return Response(content=PWA_ICON_SVG, media_type="image/svg+xml")
385
+
386
+ if __name__ == "__main__":
387
+ demo.launch()