LZ-SG/embedded-dev-tutorials
0
1#!/usr/bin/env python3
2import re
3
4path = r"C:\Users\LYS\.qclaw\workspace\embedded-dev-site\static\index.html"
5with open(path, 'r', encoding='utf-8') as f:
6 content = f.read()
7
8# 1. Add "time" field to each cardsData entry
9# Replace: , size:'112KB' → , time:'~60分钟', size:'112KB'
10time_map = {
11 "main": "~60分钟", "arm-gcc": "~20分钟", "cmake": "~20分钟",
12 "makefile": "~25分钟", "flash-debug": "~30分钟", "serial": "~20分钟",
13 "stm32": "~35分钟", "esp32": "~20分钟", "freertos": "~40分钟",
14 "protocols": "~20分钟", "practice-projects": "~10分钟", "bci-2026": "~15分钟",
15 "proj-led-button": "~15分钟", "proj-uart-hello": "~15分钟",
16 "proj-oled-display": "~15分钟", "proj-dht11-monitor": "~12分钟",
17 "proj-pwm-motor": "~18分钟", "proj-servo-control": "~18分钟",
18 "proj-ir-remote": "~15分钟", "proj-ultrasonic": "~15分钟",
19 "proj-stepper-motor": "~18分钟", "proj-freertos-station": "~20分钟",
20 "proj-smart-car": "~25分钟", "proj-can-bus": "~20分钟",
21 "proj-sd-logger": "~20分钟", "proj-esp32-iot": "~20分钟",
22 "proj-low-power": "~18分钟", "proj-bootloader-iap": "~20分钟",
23 "proj-dma-adc": "~18分钟", "proj-usb-hid": "~20分钟",
24 "proj-s32k144-auto": "~20分钟", "proj-oscilloscope-debug": "~15分钟"
25}
26
27for page_id, time_val in time_map.items():
28 # Match: page:'xxx', ... other fields ... but before size:
29 pattern = rf"(page:'{page_id}',[^}}]*?), size:"
30 replacement = rf"\1, time:'{time_val}', size:"
31 content = re.sub(pattern, replacement, content)
32
33# 2. Update card rendering to show time
34content = content.replace(
35 "<div class=\"size\">${c.size}</div>",
36 "<div class=\"size\">${c.size} <span class=\"read-time\">⏱ ${c.time||''}</span></div>"
37)
38
39# 3. Add sort bar before home-grid rendering
40old_sort = "function renderCardGroup(title, items) {"
41new_sort_block = """let sortMode = 'default';
42function renderSortBar() {
43 return `<div class="sort-bar"><span class="label">排序:</span>
44 <button class="sort-btn active" onclick="sortCards('default',this)">默认</button>
45 <button class="sort-btn" onclick="sortCards('time-asc',this)">⏱ 短→长</button>
46 <button class="sort-btn" onclick="sortCards('time-desc',this)">⏱ 长→短</button>
47 <button class="sort-btn" onclick="sortCards('diff-asc',this)">📊 简单→难</button>
48 <button class="sort-btn" onclick="sortCards('name',this)">🔤 名称</button>
49 </div>`;
50}
51function sortCards(mode, btn) {
52 document.querySelectorAll('.sort-btn').forEach(b=>b.classList.remove('active'));
53 if(btn) btn.classList.add('active');
54 sortMode = mode;
55 loadPage('home');
56}
57function getSorted(items) {
58 if (sortMode === 'time-asc') return [...items].sort((a,b)=>parseInt(a.time||'99')-parseInt(b.time||'99'));
59 if (sortMode === 'time-desc') return [...items].sort((a,b)=>parseInt(b.time||'99')-parseInt(a.time||'99'));
60 if (sortMode === 'diff-asc') return [...items].sort((a,b)=>(a.diff||0)-(b.diff||0));
61 if (sortMode === 'name') return [...items].sort((a,b)=>a.title.localeCompare(b.title,'zh'));
62 return items;
63}
64function renderCardGroup(title, items) {
65 items = getSorted(items);"""
66
67content = content.replace(old_sort, new_sort_block)
68
69# 4. Add sort bar after home-stats div in renderHome
70old_stats_end = "`;</div>`;\n\n // 分组"
71if old_stats_end in content:
72 content = content.replace(
73 old_stats_end,
74 "`;</div>`;\n\n html += renderSortBar();\n\n // 分组"
75 )
76
77# 5. Add bookmark JS functions before // === 初始化 ===
78bookmark_js = """// === 收藏夹 ===
79function getBookmarks() {
80 try { return JSON.parse(localStorage.getItem('tutorial_bookmarks')||'[]'); }
81 catch(e) { return []; }
82}
83function saveBookmarks(list) { localStorage.setItem('tutorial_bookmarks', JSON.stringify(list)); }
84function toggleBookmark(pageId) {
85 const bm = getBookmarks();
86 const idx = bm.indexOf(pageId);
87 if (idx >= 0) bm.splice(idx, 1);
88 else bm.push(pageId);
89 saveBookmarks(bm);
90 updateBookmarkUI();
91 renderBookmarksList();
92}
93function isBookmarked(pageId) { return getBookmarks().includes(pageId); }
94function updateBookmarkUI() {
95 const btn = document.getElementById('bmToggle');
96 if (!btn) return;
97 if (isBookmarked(currentPage)) { btn.classList.add('active'); btn.textContent = '⭐'; }
98 else { btn.classList.remove('active'); btn.textContent = '☆'; }
99}
100function renderBookmarksList() {
101 const bm = getBookmarks();
102 const section = document.getElementById('bookmarksSection');
103 const list = document.getElementById('bookmarkList');
104 const countEl = document.getElementById('bmCount');
105 if (bm.length === 0) { section.style.display = 'none'; return; }
106 section.style.display = 'block';
107 countEl.textContent = bm.length;
108 let html = '';
109 bm.forEach(id => {
110 const cfg = PAGES[id];
111 if (cfg) html += `<div class="bm-item" onclick="loadPage('${id}')">${cfg.title}<span class="bm-remove" onclick="event.stopPropagation();removeBookmark('${id}')">✕</span></div>`;
112 });
113 list.innerHTML = html;
114}
115function removeBookmark(pageId) {
116 const bm = getBookmarks().filter(id => id !== pageId);
117 saveBookmarks(bm);
118 renderBookmarksList();
119 updateBookmarkUI();
120}
121
122// === 学习进度 ===
123function markViewed(pageId) {
124 if (pageId === 'home') return;
125 try {
126 const viewed = JSON.parse(localStorage.getItem('tutorial_viewed')||'{}');
127 viewed[pageId] = new Date().toISOString();
128 localStorage.setItem('tutorial_viewed', JSON.stringify(viewed));
129 } catch(e) {}
130}
131function getViewedCount() {
132 try { return Object.keys(JSON.parse(localStorage.getItem('tutorial_viewed')||'{}')).length; }
133 catch(e) { return 0; }
134}
135function getProgressPercent() {
136 const total = Object.keys(PAGES).length - 1; // exclude home
137 return Math.round((getViewedCount() / total) * 100);
138}
139function progressIndicator() {
140 const pct = getProgressPercent();
141 return pct > 0 ? `<div class="progress-indicator" title="已读 ${pct}%"><div class="pi-bar"><div class="pi-fill" style="width:${pct}%"></div></div>${pct}%</div>` : '';
142}
143function updateHomeStats() {
144 const viewed = getViewedCount();
145 const progressHtml = progressIndicator();
146 // Update stats if on home page
147 const statsDiv = document.querySelector('.home-stats');
148 if (statsDiv) {
149 const existing = statsDiv.querySelector('.home-stat.progress-stat');
150 if (!existing && viewed > 0) {
151 const statEl = document.createElement('div');
152 statEl.className = 'home-stat progress-stat';
153 statEl.innerHTML = `<div class="num">${viewed}</div><div class="label">已学习</div><div class="progress-indicator" style="margin-top:4px"><div class="pi-bar"><div class="pi-fill" style="width:${getProgressPercent()}%"></div></div>${getProgressPercent()}%</div>`;
154 statsDiv.appendChild(statEl);
155 } else if (existing && viewed > 0) {
156 existing.querySelector('.num').textContent = viewed;
157 existing.querySelector('.pi-fill').style.width = getProgressPercent()+'%';
158 existing.querySelector('.progress-indicator').childNodes[2].textContent = getProgressPercent()+'%';
159 }
160 }
161}
162
163"""
164
165insert_at = "// === 初始化 ==="
166content = content.replace(insert_at, bookmark_js + "\n" + insert_at)
167
168# 6. Update loadPage to add bookmark button, mark viewed, and render bookmarks list
169# Add bookmark button after breadcrumb update
170old_bm = "// 关闭移动端侧边栏\n document.getElementById('sidebar').classList.remove('open');"
171new_bm = """// 关闭移动端侧边栏
172 document.getElementById('sidebar').classList.remove('open');
173
174 // 更新收藏按钮
175 const bmToggle = document.getElementById('bmToggle');
176 if (bmToggle) {
177 if (pageId === 'home') { bmToggle.style.display = 'none'; }
178 else { bmToggle.style.display = ''; updateBookmarkUI(); }
179 }
180
181 // 标记已读
182 if (pageId !== 'home') markViewed(pageId);"""
183content = content.replace(old_bm, new_bm)
184
185# 7. Add bookmark toggle button in topbar (after search-box)
186old_search = '<input type="text" id="searchInput" placeholder="搜索教程内容..." oninput="handleSearch(this.value)">\n </div>'
187new_search = '<input type="text" id="searchInput" placeholder="搜索教程内容..." oninput="handleSearch(this.value)">\n </div>\n <button class="bookmark-btn" id="bmToggle" onclick="toggleBookmark(currentPage)" title="收藏当前教程" style="display:none">☆</button>'
188content = content.replace(old_search, new_search)
189
190# 8. Update init to load bookmarks
191old_init = "loadPage('home');"
192new_init = "loadPage('home');\n renderBookmarksList();\n updateHomeStats();"
193content = content.replace(old_init, new_init)
194
195# 9. Add renderBookmarksList + updateHomeStats calls in loadPage for home
196old_home_return = "history.pushState({page:pageId}, '', `#/${pageId}`);\n return;"
197new_home_return = "history.pushState({page:pageId}, '', `#/${pageId}`);\n updateHomeStats();\n setTimeout(() => updateHomeStats(), 200);\n return;"
198content = content.replace(old_home_return, new_home_return)
199
200# 10. After tutorial loads, call updateBookmarkUI and show bookmark button
201old_postload = "content.appendChild(navHtml);"
202new_postload = "content.appendChild(navHtml);\n // 显示收藏按钮\n const bmBtn = document.getElementById('bmToggle');\n if (bmBtn) { bmBtn.style.display = ''; updateBookmarkUI(); }"
203content = content.replace(old_postload, new_postload)
204
205with open(path, 'w', encoding='utf-8', newline='') as f:
206 f.write(content)
207
208print("Done! Statistics:")
209print(f" Total chars: {len(content)}")
210print(f" Has bookmark JS: {'getBookmarks' in content}")
211print(f" Has time fields: {'time:' in content}")
212print(f" Has sort bar: {'renderSortBar' in content}")
213print(f" Has bmToggle: {'bmToggle' in content}")
214print(f" Has markViewed: {'markViewed' in content}")
215 