sync: collector/ 同步自 inkstone/arcrun-rag@c6cf717(桌面小幫手 0.18.38)
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"_algo": 4,
|
||||
"version": "0.18.37",
|
||||
"fingerprint": "3bb5c94f68a26a06",
|
||||
"version": "0.18.38",
|
||||
"fingerprint": "7add5da65c671548",
|
||||
"files": {
|
||||
".gitignore": "4d56952b0fb13bf8f9b6c13a6d4c34a075bac3af447636a1df4335d7576e2f97",
|
||||
"CHANGELOG.md": "ba7a2a7cc8071e62c7493057abff43c81739c7828332a9124fcb415263b3b649",
|
||||
"CHANGELOG.md": "9dd02f82a4f5417520fb20642aec004aa25c241e404621aacf6f37e549602b2c",
|
||||
"DAEMON_LINE": "d5019abbdc8a5f2919e9e3510391891cd7fbdf0765bf16ec83caa779f370116d",
|
||||
"README.md": "9d92cac236b20a0b183eea3e7f5e39ad492f05192c4ea602eb11c3d09967327f",
|
||||
"check-standalone.sh": "65fbce096326791c2f103a51e76d0ad79e9e510e2a7700b49ac2d2d81f73c853",
|
||||
@@ -19,7 +19,7 @@
|
||||
"cloud_version_test.go": "66c004de1f0a3958851f22a22367065f13b3381ac7d936ae54d812c26a0b9fb6",
|
||||
"cmd/arcrun-app/.gitignore": "7502ba597654ebb3463535bdfb51c271abad666a67c7ac8ab36deca1a72615f8",
|
||||
"cmd/arcrun-app/README.md": "23e53fdf53fcd96b6d8d7a121eb91d3242d11b69d655770473b745c49bb387b9",
|
||||
"cmd/arcrun-app/app.go": "d7552348ea2c662bceabd866a33650c69278420374fc95200c1c86270a6e46b8",
|
||||
"cmd/arcrun-app/app.go": "274129ed77c4f4194f6b2007cb47e4ffe536b15b6450fe89b14fbb31e6a16395",
|
||||
"cmd/arcrun-app/apps.go": "0df6da9d55adbdec500fbb01005a305287d1dbf796b7e463f2b2f222626b4c96",
|
||||
"cmd/arcrun-app/apps_test.go": "0e2f137f8b49f421e869ab3192ca12c8bb756029d8730487ccc44733aaf7f96c",
|
||||
"cmd/arcrun-app/arcrun-app": "e2bdf54a15d69860df05820274a80d7497b7d62a4e8ba219f72f30b543456b79",
|
||||
@@ -88,8 +88,8 @@
|
||||
"cmd/arcrun-app/frontend/src/assets/fonts/OFL.txt": "8656c2a27357b3140047a39b27e3cd928256544ad8a004ef77087f761358208c",
|
||||
"cmd/arcrun-app/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2": "06f3af3fe52542d40ad9bc14ec03e04deaabd09ec369221cc8f536db1c72bf55",
|
||||
"cmd/arcrun-app/frontend/src/assets/images/logo-universal.png": "157a874acd3363634747287c0b01034e9f1eec838a4cf39827dfea789e730494",
|
||||
"cmd/arcrun-app/frontend/src/main.js": "26c4ecf26b848916c96620a0e7aa6d91f1da208dd96597c08f39717b4e4eddee",
|
||||
"cmd/arcrun-app/frontend/src/style.css": "24b3797c731720b60ce8a2a19582b4a76e8e37a9d2eacce8dbe205ba4008849b",
|
||||
"cmd/arcrun-app/frontend/src/main.js": "277390ab8bf4114c797aa32b2cdae151e86a63681c612c424e73c308c446234e",
|
||||
"cmd/arcrun-app/frontend/src/style.css": "4fb87b139100b826c02c71a69599da2c4e8b93d3f7855c2a9b10e6677fecfbb2",
|
||||
"cmd/arcrun-app/gen-icons.py": "df5b3b33e500fc564badfc8bd2a9ca6a43f7d7e3b31111c41e3b322137c4c96c",
|
||||
"cmd/arcrun-app/go.mod": "8b9e57bcfd44886d1ec06480e033af706ff3fed470786c57074900dd48ecf86f",
|
||||
"cmd/arcrun-app/go.sum": "bc4453654c203e5af251c5a3061616cb0ceeb1b6a78a02ace2ca85524d217256",
|
||||
@@ -122,11 +122,11 @@
|
||||
"convert_table_test.go": "d0371b7566ef3152f9dd42f9f990e0dffa1c50a0c8e874a28415fa2e4c188394",
|
||||
"convert_test.go": "04f3fa30d1be5f910c0e0be3308ced2963191ab030a9eaccd986ef581fcd4e18",
|
||||
"convert_wiring_test.go": "09e97bf32ace245b55acc7d65ee5c0bb7fd84f8fb1aab61f603f9e9d18b22f13",
|
||||
"direct.go": "1cf3a57ab5016a0fe7ad30c3e36789825f705de93456faeb237bee5e45e1c6ac",
|
||||
"direct_extract_test.go": "d3465c1b5953858ae780d192a2cf515d452f48eee3ff18a6345ce64320debbbb",
|
||||
"direct_multi_test.go": "d0aa211d90aaaa9d7dbe2b04f1aaa5989e91e91b037144cc0f4e0fd068f43c2b",
|
||||
"direct.go": "70ff75a04cdfff09453ff858d3e468e110057e555c402e5969221072fefe1a41",
|
||||
"direct_extract_test.go": "317d0057d2d368fabc406351a7477f72e074d2b8c8459b1dc021febdf16902f4",
|
||||
"direct_multi_test.go": "a76593fef26231e18b7402f6f7376624d2471f8a719b3ce08fc4a0f5a3d3cf1e",
|
||||
"direct_pacing.go": "2690b2fff858250028886bc3cd7793be54b4d4a03ddca41485476cb04b79f7c3",
|
||||
"direct_pacing_test.go": "c21298922866eb9dabc6a87d9d466e08e1001db52841c86f3e720dacfe955bd8",
|
||||
"direct_pacing_test.go": "ba5c140082931a50c3728078502803c654c2fb9dacecda679f8f40e6ab61a0c5",
|
||||
"direct_quota_test.go": "0d4747db434fd10f367ff88c8253263e2f2ce3559cd767de2f0b2c77bdf7a080",
|
||||
"direct_retire_test.go": "fbff728362313c546c1fc01cc74094672e22a8171d4bc0329ce1b3d118c82b3c",
|
||||
"direct_sync_now_test.go": "dc7a6e3d5646a470e889d30f0224cdb86be832b8cf85ffef8629e42ec577622b",
|
||||
@@ -141,6 +141,9 @@
|
||||
"folderindex_helpers_test.go": "2cf17b70c396225f728e8e160cfc75d7bdc76ac1ce8e574d1450b020050c0734",
|
||||
"folderindex_test.go": "a5319eb1e92473cb5c32d3beddb7889997556227824de300dd39953b43beab70",
|
||||
"foldershape.go": "b1b237f5674674efdc9bdea849b63c571a014e64bec33af42fb7c4769fa7582b",
|
||||
"foldertree.go": "8b3de4fde32e538b3a6a9440f6fb89e5737391a2499668b961a933509b36fa17",
|
||||
"foldertree_local_test.go": "ce919dc2a21ec96716e3d43a7f493367346e212243b4ab22c2d960fc26408f3a",
|
||||
"foldertree_test.go": "f8b8cdf1ef9d7d1c80cabb086fc9b4f2162b91a47d6d78bde2632b07672c0b88",
|
||||
"go.mod": "05dacc224af2a6797db86689435f83537efb433ffba521467b95d1099f5e12db",
|
||||
"go.sum": "a9c92957ca368462aab32a82a6f51c9586ea07478a7967012986d757e9969ab3",
|
||||
"grounding.go": "7152771ab1bb152946209f8e94e52bdf2facc6192b7a8ca018e37fed4557e9c0",
|
||||
@@ -151,14 +154,14 @@
|
||||
"ingestplan_test.go": "1ad31f4fb69d917e3f9d46f5615f38074b7ed6ec8baa01d71bd1600d317b7878",
|
||||
"ingestplan_wiring_test.go": "4e5d25dace8a181418ed5857423ef1efeacdd98b7ff42b94a71c4e5fd7012e89",
|
||||
"inventory.go": "d0ba0be3e2af8df3cc6dffac5b75816b9c2a0a12f6d2eff1a786d8584ca77622",
|
||||
"inventory_test.go": "1821c81cf2b9e20fb82a79313686393391a6f46185dfa4a4a4c2bfb4e0e7c170",
|
||||
"inventory_test.go": "16e0f83a09d61dc4cded032fd493d30bfc747c38116b871fb02b2f0bf2b0216a",
|
||||
"lint.go": "63af1289978a02c9722ac8bb934a916b9e4e65cffcfec0749eb07590496960f3",
|
||||
"lint_test.go": "2f57a04b9d697fba2d078eb08f49d2fec1640c7e3c61a60bc3821d2bb0798153",
|
||||
"machineid.go": "d4224e684ecfbafe2efc7e0b95377f9e669ee1256a442aa72ce2cab3b219fc56",
|
||||
"machineid_test.go": "5b54ac62ee1fb28b8f0a873888aeddb844af0aa74472bfb849573cfc6691a290",
|
||||
"machinemark.go": "b93464c2ee89a73c0e0da38bcce5dc1ba6c1deba2fc483f28c6021c3ffdee4a0",
|
||||
"main.go": "fe2f69739f3ee92713f8aaca925068ecf97b33d476eee826fc1e0d5def8f0d10",
|
||||
"manifest.go": "cbfaf5fc83b5a0ff8c2a62d776bb1a8e0d99367ec31a7fbc4081bd56d848c145",
|
||||
"manifest.go": "61a9403a3da46f270ce6a1610b9307fd19b794521b71a7a8f22cea36a6ea35cd",
|
||||
"manifest_extractedby_test.go": "8019fe35675a1e801b7bda829ee1d647de32db03c029b1c651f71573e644c69e",
|
||||
"manifest_retry_test.go": "e6b4a321ccddfeed8cdbfabe6d031d5a9c59a33b47baf0b6d21150fa8406748a",
|
||||
"multiaccount_connected_test.go": "ba1817b73a5dd0dd5d300750e54d5c53bd82a75ef6e43caeddfcb2303d64ff66",
|
||||
@@ -173,7 +176,7 @@
|
||||
"repoguard_test.go": "ce0316a59af944c3cf391961653fe82294fd45e728d35b4b7fcd99b2b20e7b5c",
|
||||
"safewrite.go": "d5dd09ffe894fb9fb94656a4bb50d246bd8c65ab21a7ad81463f43dfdacc8a57",
|
||||
"safewrite_test.go": "2980f0f86f78fb265403e31d4cba6da43f4dbc16e7c4711cfccba23175578358",
|
||||
"scan.go": "03a15d7dac31a4853e1bf1baf760da6d26d708e2805a533ccef5d117a438856e",
|
||||
"scan.go": "d26b74e46204697a573ed24ee427c06878349611fcaf3c35df295b826b690525",
|
||||
"scan_carry_lasterror_test.go": "6fa542422fb3e00b5ec4355b5db8df7086361b2dacefd67755d75c0ddfa55575",
|
||||
"scan_dedup_scale_test.go": "573b3c78bec86614bf4d2f99b7c3c3157f92c24af73cae7290428fc7f43f0362",
|
||||
"scan_dedup_test.go": "580ec58596ca54aa3d29b417e7d098fd2e3455fd39bf10e7da02d453c0786df0",
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"0.18.34": "d8c7490b9d32e62e",
|
||||
"0.18.35": "3bd7f3fb0124bda4",
|
||||
"0.18.36": "abb2f2cc2498f071",
|
||||
"0.18.37": "3bb5c94f68a26a06"
|
||||
"0.18.37": "3bb5c94f68a26a06",
|
||||
"0.18.38": "7add5da65c671548"
|
||||
}
|
||||
|
||||
@@ -927,6 +927,57 @@ func (a *App) OpenLogFolder() error {
|
||||
// LogFolderPath 給畫面顯示用(讓使用者就算按鈕失效也知道去哪找)。
|
||||
func (a *App) LogFolderPath() string { return appDir() }
|
||||
|
||||
// ── 資料夾結構(`inkstone/InkStoneCo#44`,leo 2026-08-26)──
|
||||
//
|
||||
// leo 的交付定義第一段:「在 Portal 和**桌面小幫手**上,任何一個連上的資料夾
|
||||
// 都攤得開它完整的巢狀子資料夾樹,每一層看得到這層有幾份、同步了幾份、
|
||||
// 沒同步的那幾份為什麼沒上去。」——這支是桌面那半的資料入口。
|
||||
//
|
||||
// 🔴 **三件刻意不做的事**,每一件都是紅線:
|
||||
//
|
||||
// ① 不自己走一次檔案系統。樹是 collector 每輪算好落地的(BuildFolderTree ⇒
|
||||
// folder-trees.json),這裡只讀。App 若自己數一次,就會出現「畫面說 A、
|
||||
// 雲端說 B」——同一個資料夾兩份實作,遲早對不起來。
|
||||
// ② 不去雲端拿。樹本來就是本機算的;繞一趟雲端只是把離線、額度用完、
|
||||
// 退避窗口這幾種情況都變成「畫面壞掉」。
|
||||
// ③ 不掛在 GetState 上。GetState 每秒被 tick 呼叫一次,而樹上限 300 個節點;
|
||||
// 只有使用者真的按開那個資料夾時才需要它 ⇒ 獨立一支、按需呼叫。
|
||||
//
|
||||
// 回傳 nil 代表「這個資料夾還沒被回報過」(剛加入、還沒跑完第一輪),
|
||||
// **不是**空樹——畫面要分得出「還沒掃到」與「掃過但裡面是空的」(arcrun-rag#106
|
||||
// 那條紅線的同一個形狀:不准拿一個我們自己編的 0 去回答使用者)。
|
||||
func (a *App) GetFolderTree(path string) (*collector.FolderTree, error) {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
// 快照與 manifest 同目錄。manifest 路徑以設定檔為準(使用者可能把它指到別處),
|
||||
// 讀不到設定就退回預設位置——與 statusPath() 同一個假設。
|
||||
store := filepath.Join(appDir(), "folder-trees.json")
|
||||
if cfg, err := loadCfg(); err == nil && strings.TrimSpace(cfg.Manifest) != "" {
|
||||
store = collector.FolderTreeStorePath(cfg.Manifest)
|
||||
}
|
||||
s, err := collector.LoadFolderTreeStore(store)
|
||||
if err != nil {
|
||||
// 檔案還不存在=第一輪還沒跑完,那不是錯誤,是「還沒有」。
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if t, ok := s.Trees[path]; ok {
|
||||
return &t, nil
|
||||
}
|
||||
// 設定裡存的路徑與 collector 寫入時用的 absRoot 可能一個帶了尾斜線、
|
||||
// 一個沒有(AddFolder 直接存系統選擇器給的字串)⇒ 正規化後再找一次,
|
||||
// 不要因為一個斜線就跟使用者說「還沒回報」。
|
||||
if abs, err := filepath.Abs(path); err == nil {
|
||||
if t, ok := s.Trees[abs]; ok {
|
||||
return &t, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ── 托盤會呼叫的兩個動作(t194)──
|
||||
|
||||
// ShowWindow 把主視窗叫出來並帶到前景。
|
||||
|
||||
@@ -296,6 +296,217 @@ function cardSkipped(k) {
|
||||
}
|
||||
|
||||
// ── 各庫頁:動作全部作用在這個庫(不會加錯帳號)──
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 資料夾結構樹(`inkstone/InkStoneCo#44` 桌面那半,leo 2026-08-26)
|
||||
//
|
||||
// leo 的交付定義第一段:「在 Portal 和**桌面小幫手**上,任何一個連上的資料夾
|
||||
// 都攤得開它完整的巢狀子資料夾樹,每一層看得到這層有幾份、同步了幾份、
|
||||
// 沒同步的那幾份為什麼沒上去。」Portal 那半已經在跑,這裡是桌面這半。
|
||||
//
|
||||
// 🔴 形狀規格=`inkstone/Arcrun#144`(leo 2026-08-19 驗 1.4.49 之後打回 markmap):
|
||||
// 「我的需求是**向右向下**,類似 terminal 的 tree,**一列一列向下往後退縮**,
|
||||
// 緊湊但可點擊展開,**模擬 Windows 的檔案總管的 tree**,這是一般辦公室用戶
|
||||
// 都有的經驗,且可以用滑鼠輕易操作,不會佔用大面積。」
|
||||
// ⇒ 一列一行、縮排、可摺疊、字級行距貼近作業系統的檔案總管。
|
||||
// ⇒ **不用任何樹狀圖套件**:這個形狀就是「縮排的清單」,一個 div 一列就到位;
|
||||
// 引一包 library 進來只會多一份要跟著 CIS 對齊的樣式來源。
|
||||
//
|
||||
// 🔴 **一個判準都不在這裡發明**(同 portal 那半的紅線):兩個數字與「為什麼沒收」
|
||||
// 的分類全部由 collector 算好(`collector/foldertree.go`,判準活在 scan.go 那趟
|
||||
// WalkDir)。這裡只做兩件事:把節點串成樹、把子樹的數字加起來。
|
||||
// 哪天想在這裡寫「.py 算不算支援」——停手,那是第二份實作。
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// data:path → 樹(undefined=還沒問、null=問過但小幫手還沒回報)
|
||||
// open:path → 這個資料夾的樹展開了沒
|
||||
// nodes:path → { 節點路徑: 展開了沒 }(沒有紀錄=用預設,見 nodeIsOpen)
|
||||
const treeState = { data: {}, open: {}, nodes: {} };
|
||||
|
||||
// 資料夾路徑當不了 DOM id(含空白、斜線、中文)⇒ 折成一個穩定的短碼。
|
||||
function treeBoxId(path) {
|
||||
let h = 0;
|
||||
for (let i = 0; i < path.length; i++) h = (h * 31 + path.charCodeAt(i)) | 0;
|
||||
return 'ft' + (h >>> 0).toString(36);
|
||||
}
|
||||
|
||||
// 子樹合計:小幫手送的每個數字都只算「這一層直接放的檔」(存兩套遲早對不起來),
|
||||
// 所以要顯示「這個資料夾底下總共」就得在這裡疊一次。
|
||||
// 🔴 skipped 的節點整棵不計:沒走進去就是不知道裡面有幾個檔,加 0 會讓分母說謊。
|
||||
// (與 portal 的 rollupTree 同一套算法——兩邊看到的數字必須是同一個。)
|
||||
function rollupTree(nodes) {
|
||||
const byPath = {}, kids = {};
|
||||
nodes.forEach((n) => { byPath[n.path] = n; (kids[n.parent] = kids[n.parent] || []).push(n); });
|
||||
const sums = {};
|
||||
function walk(n) {
|
||||
const acc = { total: 0, synced: 0, pending: 0, unsupported: 0, excluded: 0 };
|
||||
if (!n.skipped) {
|
||||
acc.total = n.total_files; acc.synced = n.synced_files; acc.pending = n.pending_files;
|
||||
acc.unsupported = n.unsupported_files; acc.excluded = n.excluded_files;
|
||||
}
|
||||
(kids[n.path] || []).forEach((c) => {
|
||||
const x = walk(c);
|
||||
acc.total += x.total; acc.synced += x.synced; acc.pending += x.pending;
|
||||
acc.unsupported += x.unsupported; acc.excluded += x.excluded;
|
||||
});
|
||||
sums[n.path] = acc;
|
||||
return acc;
|
||||
}
|
||||
// 根=parent 是 '-'(collector/foldertree.go 刻意用它,才分得出「我是根」與「父親是根」)
|
||||
(kids['-'] || []).forEach(walk);
|
||||
// 孤兒節點(父親不在這份清單裡,例如樹被截斷)也要走一次,否則它整棵不會被算到
|
||||
nodes.forEach((n) => { if (sums[n.path] === undefined && !byPath[n.parent]) walk(n); });
|
||||
return { sums, kids, byPath };
|
||||
}
|
||||
|
||||
// 差額必須解釋得了:總數 − 已同步 = 不支援 + 不收 + 處理中。
|
||||
// 這行文案的存在理由就是 leo 那句「不上傳通常是不支援,比如程式碼、不支援的格式」
|
||||
// ——畫面要自己回答,不必問人。
|
||||
function gapWhy(s) {
|
||||
const parts = [];
|
||||
if (s.unsupported > 0) parts.push(s.unsupported + ' 份格式還讀不了');
|
||||
if (s.excluded > 0) parts.push(s.excluded + ' 份不在收檔範圍(程式碼等)');
|
||||
if (s.pending > 0) parts.push(s.pending + ' 份處理中');
|
||||
return parts.join('・');
|
||||
}
|
||||
|
||||
// 預設只展開根那一層——**照檔案總管的行為**:打開一個資料夾看到它底下一層,
|
||||
// 要更深自己點。一次全攤開在 8000 個檔的資料夾上就是一面沒人看得完的牆。
|
||||
function nodeIsOpen(path, n) {
|
||||
const st = treeState.nodes[path] || {};
|
||||
return st[n.path] === undefined ? n.depth === 0 : st[n.path];
|
||||
}
|
||||
|
||||
function renderFolderTree(path) {
|
||||
const box = $(treeBoxId(path));
|
||||
if (!box) return;
|
||||
const tree = treeState.data[path];
|
||||
if (tree === undefined) { box.innerHTML = `<div class="ftmsg">讀取中…</div>`; return; }
|
||||
// 🔴 分得出「還沒掃到」與「掃過、裡面是空的」——後者是 arcrun-rag#106 的正常情況
|
||||
// (指定了空資料夾,它就該在畫面上存在),前者是「再等一下」。
|
||||
// 兩者講同一句話,等於拿我們自己編的答案回答使用者。
|
||||
if (tree === null) {
|
||||
box.innerHTML = `<div class="ftmsg">同步小幫手還沒掃到這個資料夾的結構。
|
||||
第一次同步跑完之後就會出現——檔案多的話要等久一點,按上面的「立刻同步」可以催它。</div>`;
|
||||
return;
|
||||
}
|
||||
const nodes = tree.nodes || [];
|
||||
if (!nodes.length) {
|
||||
box.innerHTML = `<div class="ftmsg">這個資料夾目前沒有任何內容(指定了就會在這裡,等你放東西進去)。</div>`;
|
||||
return;
|
||||
}
|
||||
const r = rollupTree(nodes);
|
||||
let html = '';
|
||||
function emit(n) {
|
||||
const s = r.sums[n.path] || { total: 0, synced: 0, pending: 0, unsupported: 0, excluded: 0 };
|
||||
const kids = (r.kids[n.path] || []).slice().sort((a, b) => (a.path < b.path ? -1 : 1));
|
||||
const open = kids.length ? nodeIsOpen(path, n) : false;
|
||||
// 縮排 16px 一層=檔案總管的量級;一列一行、往後退縮(Arcrun#144 的形狀)。
|
||||
const indent = 4 + n.depth * 16;
|
||||
// 🔴 三角形用 CSS 畫(`<i>` 那個空元素),**不用 ▸▾ 字元**:
|
||||
// 那兩個字在 Windows 的中文字型裡會被當成全形符號、大小與位置各機器不同,
|
||||
// 而它正是 leo 2026-08-19 點名「展開字很小、滑鼠不好按」的那個東西。
|
||||
// 畫出來的三角形每一台都一樣大,也才控得住點擊區。
|
||||
// 可展開的那幾列是**按鈕**,不是裝飾用的 div:標上 role/tabindex/aria-expanded
|
||||
// ⇒ 鍵盤按得到、輔助技術念得出「收合/展開」,機械檢查也看得見它是可操作的。
|
||||
let row = `<div class="ftrow${kids.length ? ' has' : ''}${open ? ' open' : ''}" style="padding-left:${indent}px"`
|
||||
+ (kids.length
|
||||
? ` data-tnode="${esc(n.path)}" data-troot="${esc(path)}" role="button" tabindex="0"`
|
||||
+ ` aria-expanded="${open}" title="${open ? '收合' : '展開'}「${esc(n.name || '')}」"`
|
||||
: '') + `>`
|
||||
+ `<span class="tw">${kids.length ? '<i></i>' : ''}</span>`
|
||||
+ `<span class="ic">${kids.length && open ? '📂' : '📁'}</span>`
|
||||
+ `<span class="nm">${esc(n.name || '(未命名)')}</span>`;
|
||||
if (n.skipped) {
|
||||
// 🔴 整棵沒走進去 ⇒ 不准顯示 0/0(那會是我們自己編的數字),改講理由。
|
||||
const why = n.skip_reason || '(小幫手沒說明理由)';
|
||||
row += `<span class="skip">整個資料夾未收</span>`
|
||||
+ `<span class="why" title="${esc(why)}">${esc(why)}</span>`;
|
||||
} else {
|
||||
const full = s.total > 0 && s.synced === s.total;
|
||||
row += `<span class="num${full ? ' full' : ''}">${s.synced} / ${s.total}</span>`;
|
||||
const why = gapWhy(s);
|
||||
if (why) row += `<span class="why" title="${esc(why)}">${esc(why)}</span>`;
|
||||
}
|
||||
if (kids.length && !open) row += `<span class="why">…還有 ${kids.length} 個子資料夾</span>`;
|
||||
html += row + `</div>`;
|
||||
if (open) kids.forEach(emit);
|
||||
}
|
||||
(r.kids['-'] || []).forEach(emit);
|
||||
// 父親不在清單裡的孤兒(樹被截斷時會有)——照樣列出來,缺角要看得見,不要偷偷藏起來
|
||||
const seen = {};
|
||||
nodes.forEach((n) => { seen[n.path] = true; });
|
||||
nodes.forEach((n) => { if (n.parent !== '-' && !seen[n.parent]) emit(n); });
|
||||
|
||||
let foot = '';
|
||||
if (tree.truncated) {
|
||||
foot += `<div class="ftmsg">資料夾太多,只顯示前 ${nodes.length} 個(實際有 ${tree.total_nodes} 個)。</div>`;
|
||||
}
|
||||
foot += `<div class="ftmsg">數字是「已同步 / 這個資料夾底下總共」。兩者不相等是正常的——
|
||||
差額是還讀不了的格式、不在收檔範圍的檔案(程式碼等),或還在處理中。</div>`;
|
||||
box.innerHTML = `<div class="fthead">`
|
||||
+ `<button class="ghost" data-texpand="${esc(path)}">全部展開</button>`
|
||||
+ `<button class="ghost" data-tcollapse="${esc(path)}">全部收合</button>`
|
||||
+ (tree.reason ? `<span class="why">${esc(tree.reason)}</span>` : '')
|
||||
+ `</div><div class="ftbody">${html}</div>${foot}`;
|
||||
wireTree();
|
||||
}
|
||||
|
||||
// 展開/收合某個資料夾的樹。第一次展開才去讀(同 portal 的 lazy 做法),
|
||||
// 之後留在記憶體裡——它是**畫面暫存不是第二份真相源**,關掉視窗就沒了。
|
||||
async function toggleFolderTree(path) {
|
||||
treeState.open[path] = !treeState.open[path];
|
||||
const box = $(treeBoxId(path));
|
||||
const btn = document.querySelector(`[data-tree="${cssq(path)}"]`);
|
||||
if (btn) btn.textContent = (treeState.open[path] ? '▾ ' : '▸ ') + '資料夾結構';
|
||||
if (!box) return;
|
||||
box.style.display = treeState.open[path] ? '' : 'none';
|
||||
if (!treeState.open[path]) return;
|
||||
if (treeState.data[path] !== undefined) { renderFolderTree(path); return; }
|
||||
renderFolderTree(path); // 先畫「讀取中…」
|
||||
try {
|
||||
const t = await go.GetFolderTree(path);
|
||||
treeState.data[path] = t || null; // 後端回 null=還沒回報過
|
||||
} catch (e) {
|
||||
box.innerHTML = `<div class="err">讀不到這個資料夾的結構:${esc(String(e))}</div>`;
|
||||
return;
|
||||
}
|
||||
renderFolderTree(path);
|
||||
}
|
||||
|
||||
// 屬性選擇器要跳脫引號——資料夾路徑什麼字元都可能有。
|
||||
function cssq(s) { return String(s).replace(/["\\]/g, '\\$&'); }
|
||||
|
||||
function setAllTreeNodes(path, open) {
|
||||
const tree = treeState.data[path];
|
||||
if (!tree) return;
|
||||
const st = {};
|
||||
(tree.nodes || []).forEach((n) => { st[n.path] = open; });
|
||||
if (!open) st[''] = true; // 全部收合時根自己留著,不然整棵消失、看起來像壞了
|
||||
treeState.nodes[path] = st;
|
||||
renderFolderTree(path);
|
||||
}
|
||||
|
||||
function wireTree() {
|
||||
document.querySelectorAll('[data-tnode]').forEach((el) => {
|
||||
const toggle = () => {
|
||||
const root = el.dataset.troot, np = el.dataset.tnode;
|
||||
const st = treeState.nodes[root] || (treeState.nodes[root] = {});
|
||||
const tree = treeState.data[root] || { nodes: [] };
|
||||
const n = (tree.nodes || []).find((x) => x.path === np);
|
||||
st[np] = !(st[np] === undefined ? (n && n.depth === 0) : st[np]);
|
||||
renderFolderTree(root);
|
||||
};
|
||||
el.onclick = toggle;
|
||||
el.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } };
|
||||
});
|
||||
document.querySelectorAll('[data-texpand]').forEach((el) => {
|
||||
el.onclick = () => setAllTreeNodes(el.dataset.texpand, true);
|
||||
});
|
||||
document.querySelectorAll('[data-tcollapse]').forEach((el) => {
|
||||
el.onclick = () => setAllTreeNodes(el.dataset.tcollapse, false);
|
||||
});
|
||||
}
|
||||
|
||||
function pageLib(s, idx) {
|
||||
const a = s.accounts[idx];
|
||||
if (!a) return `<div class="empty"><div class="t">找不到這個知識庫</div></div>`;
|
||||
@@ -321,8 +532,10 @@ function pageLib(s, idx) {
|
||||
<div class="folder">
|
||||
<span class="path" title="${esc(f.path)}">${esc(f.path)}</span>
|
||||
<span class="tag">${f.resyncNote ? '補送中' : '自動同步中'}</span>
|
||||
<button class="ghost" data-tree="${esc(f.path)}">${treeState.open[f.path] ? '▾' : '▸'} 資料夾結構</button>
|
||||
<button class="ghost" data-rm="${esc(f.path)}" data-acc="${f.accIdx}">移除</button>
|
||||
</div>
|
||||
<div class="ftbox" id="${treeBoxId(f.path)}"${treeState.open[f.path] ? '' : ' style="display:none"'}></div>
|
||||
${f.resyncNote ? `<div class="d folder-note">${esc(f.resyncNote)}</div>` : ''}`).join('')
|
||||
|| `<div class="empty"><div class="t">這個知識庫還沒有資料夾</div>
|
||||
<div class="d">按右上的「加入資料夾」,選一個要自動整理的資料夾。</div></div>`}`;
|
||||
@@ -740,6 +953,12 @@ function wire() {
|
||||
document.querySelectorAll('[data-rm]').forEach((b) => {
|
||||
b.onclick = () => confirmRemove(Number(b.dataset.acc), b.dataset.rm);
|
||||
});
|
||||
// #44:資料夾結構。換頁/重畫之後把本來就展開著的那幾棵補回去——
|
||||
// 不補的話使用者每次切回這一頁都得重按一次(狀態在 treeState,畫面卻是空的)。
|
||||
document.querySelectorAll('[data-tree]').forEach((b) => {
|
||||
b.onclick = () => toggleFolderTree(b.dataset.tree);
|
||||
if (treeState.open[b.dataset.tree]) renderFolderTree(b.dataset.tree);
|
||||
});
|
||||
|
||||
// ── App 啟動器(arcrun-rag#137)──
|
||||
on('apConnect', showConnect);
|
||||
|
||||
@@ -412,3 +412,85 @@ input[type=text], input[type=password] {
|
||||
word-break: break-all; white-space: pre-wrap;
|
||||
}
|
||||
.wfitem .out.bad { color: var(--err); }
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════════
|
||||
資料夾結構樹(`inkstone/InkStoneCo#44` 桌面那半;形狀規格 `inkstone/Arcrun#144`)
|
||||
|
||||
leo 2026-08-19(驗 1.4.49 的 markmap 之後打回):
|
||||
「我的需求是**向右向下**,類似 terminal 的 tree,**一列一列向下往後退縮**,
|
||||
緊湊但可點擊展開,**模擬 Windows 的檔案總管的 tree**⋯⋯
|
||||
可以用滑鼠輕易操作,**不會佔用大面積**。」
|
||||
|
||||
⇒ 這一段的每個數字都在服務那句話:
|
||||
· 一列 = 一個 flex row,靠 padding-left 退縮(縮排 16px/層,同檔案總管量級)
|
||||
· 行高壓到 20px 上下、字級 13px ⇒ 1080p 一個畫面塞得下數十列
|
||||
· 三角形+資料夾 icon 在最左,數字與原因往右排,掃視時眼睛只走一條直線
|
||||
🔴 顏色一個 hex 都不准出現(本檔開頭那條規矩)——全部走 arcrun-cis.css 的變數。
|
||||
══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
.ftbox {
|
||||
margin: -3px 0 9px; padding: 8px 10px 6px;
|
||||
background: var(--well); border-radius: 10px;
|
||||
}
|
||||
.fthead {
|
||||
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||
padding-bottom: 7px; margin-bottom: 5px;
|
||||
border-bottom: 1px solid rgba(var(--ink-rgb), .08);
|
||||
}
|
||||
.fthead button { padding: 4px 9px; font-size: 12px; }
|
||||
/* 樹本體:檔案總管那種「緊湊但點得到」的密度。
|
||||
overflow-x 讓很深的樹自己捲,不去撐破右側版面。 */
|
||||
.ftbody { overflow-x: auto; padding: 1px 0; }
|
||||
.ftrow {
|
||||
display: flex; align-items: baseline; gap: 7px;
|
||||
padding: 2px 4px 2px 0; font-size: 13px; line-height: 1.5;
|
||||
white-space: nowrap; border-radius: 5px;
|
||||
}
|
||||
/* 有子資料夾的那幾列才可點——整列都是點擊區(三角形只有 12px,滑鼠不好瞄)。 */
|
||||
.ftrow.has { cursor: pointer; user-select: none; }
|
||||
.ftrow.has:hover { background: rgba(var(--amber-rgb), .08); }
|
||||
.ftrow.has:focus-visible { outline: 2px solid var(--amber); outline-offset: -2px; }
|
||||
/* 三角形:**用 CSS 畫,不用字元**。
|
||||
leo 2026-08-19 打回 markmap 的其中一條理由就是「**展開字很小**⋯⋯
|
||||
用戶要擁有觸控板,否則很難閱讀」——▸▾ 這兩個字在不同平台的中文字型裡
|
||||
大小與位置都不一樣(Windows 常被當全形處理),畫出來的則到處一致。 */
|
||||
.ftrow .tw {
|
||||
width: 14px; flex: none; align-self: center;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.ftrow .tw i {
|
||||
display: block; width: 0; height: 0;
|
||||
border-left: 5px solid rgba(var(--ink-rgb), .5);
|
||||
border-top: 4px solid transparent; border-bottom: 4px solid transparent;
|
||||
transition: transform .12s;
|
||||
}
|
||||
.ftrow.open .tw i { transform: rotate(90deg); }
|
||||
.ftrow.has:hover .tw i { border-left-color: var(--amber); }
|
||||
.ftrow .ic { flex: none; font-size: 12.5px; }
|
||||
.ftrow .nm { font-weight: 500; }
|
||||
.ftrow .num {
|
||||
font-family: ui-monospace, Menlo, monospace; font-size: 12px;
|
||||
color: rgba(var(--ink-rgb), .55);
|
||||
}
|
||||
/* 全部同步完=這一格沒有待辦,用完整墨色讓它從灰階裡浮出來(不另發明「成功色」)。 */
|
||||
.ftrow .num.full { color: var(--ink); }
|
||||
/* 🔴 選擇器**不要**綁在 .ftrow 底下:頁首那句「為什麼只收這些」也用同一個 class,
|
||||
綁死就只有樹裡面吃得到樣式、頁首那句會掉回 16px 的內文級——實撞過一次,
|
||||
畫面上那句話比整棵樹都大聲,喧賓奪主。 */
|
||||
/* 「為什麼沒上去」是次要資訊:**它可以被截斷,資料夾名字不行**。
|
||||
(整棵未收的理由是一整句話,不截的話一列會長到把樹推出畫面外。)
|
||||
截掉的部分留在 title 裡,滑鼠停著就看得到全文。 */
|
||||
.ftbox .why {
|
||||
font-size: 12px; color: rgba(var(--ink-rgb), .45);
|
||||
flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.fthead .why { flex: 1; min-width: 0; line-height: 1.6; }
|
||||
/* 整棵沒走進去:語氣是告知不是錯誤,所以不用 --err。 */
|
||||
.ftrow .skip {
|
||||
font-size: 11.5px; color: rgba(var(--ink-rgb), .5);
|
||||
border: 1px solid rgba(var(--ink-rgb), .18); border-radius: 5px; padding: 0 5px;
|
||||
}
|
||||
.ftmsg {
|
||||
padding: 6px 2px 2px; font-size: 12px; line-height: 1.65;
|
||||
color: rgba(var(--ink-rgb), .45);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user