skimmd 1.0.1 → 1.0.2
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/bin/skimmd.js +123 -0
- package/package.json +1 -1
- package/public/index.html +478 -3
package/bin/skimmd.js
CHANGED
|
@@ -297,6 +297,129 @@ async function createServer(options) {
|
|
|
297
297
|
}
|
|
298
298
|
});
|
|
299
299
|
|
|
300
|
+
// Browse directories and files
|
|
301
|
+
app.get("/api/browse", async (req, res) => {
|
|
302
|
+
const browsePath = req.query.path || process.env.HOME || "/";
|
|
303
|
+
try {
|
|
304
|
+
const resolvedPath = path.resolve(browsePath);
|
|
305
|
+
const stats = await fsp.stat(resolvedPath);
|
|
306
|
+
|
|
307
|
+
if (!stats.isDirectory()) {
|
|
308
|
+
return res.status(400).json({ error: "Path is not a directory" });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const entries = await fsp.readdir(resolvedPath, { withFileTypes: true });
|
|
312
|
+
const items = [];
|
|
313
|
+
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
// Skip hidden files/folders
|
|
316
|
+
if (entry.name.startsWith(".")) continue;
|
|
317
|
+
|
|
318
|
+
const fullPath = path.join(resolvedPath, entry.name);
|
|
319
|
+
const isDir = entry.isDirectory();
|
|
320
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
321
|
+
const isMarkdown = [".md", ".markdown", ".txt"].includes(ext);
|
|
322
|
+
|
|
323
|
+
// Only include directories and markdown files
|
|
324
|
+
if (isDir || isMarkdown) {
|
|
325
|
+
items.push({
|
|
326
|
+
name: entry.name,
|
|
327
|
+
path: fullPath,
|
|
328
|
+
isDirectory: isDir,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Sort: directories first, then files, alphabetically
|
|
334
|
+
items.sort((a, b) => {
|
|
335
|
+
if (a.isDirectory && !b.isDirectory) return -1;
|
|
336
|
+
if (!a.isDirectory && b.isDirectory) return 1;
|
|
337
|
+
return a.name.localeCompare(b.name);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
res.json({
|
|
341
|
+
current: resolvedPath,
|
|
342
|
+
parent: path.dirname(resolvedPath),
|
|
343
|
+
items,
|
|
344
|
+
});
|
|
345
|
+
} catch (error) {
|
|
346
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to browse" });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Open any markdown file by absolute path
|
|
351
|
+
app.get("/api/open-file", async (req, res) => {
|
|
352
|
+
const filePath = req.query.path;
|
|
353
|
+
if (!filePath || typeof filePath !== "string") {
|
|
354
|
+
return res.status(400).json({ error: "Missing file path" });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const resolvedPath = path.resolve(filePath);
|
|
359
|
+
const stats = await fsp.stat(resolvedPath);
|
|
360
|
+
|
|
361
|
+
if (!stats.isFile()) {
|
|
362
|
+
return res.status(400).json({ error: "Path is not a file" });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const content = await fsp.readFile(resolvedPath, "utf-8");
|
|
366
|
+
const html = marked.parse(content);
|
|
367
|
+
|
|
368
|
+
res.json({
|
|
369
|
+
path: resolvedPath,
|
|
370
|
+
name: path.basename(resolvedPath),
|
|
371
|
+
content,
|
|
372
|
+
html,
|
|
373
|
+
metadata: {
|
|
374
|
+
size: stats.size,
|
|
375
|
+
modifiedAt: stats.mtime,
|
|
376
|
+
createdAt: stats.birthtime,
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
} catch (error) {
|
|
380
|
+
res.status(404).json({ error: error instanceof Error ? error.message : "File not found" });
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Save any markdown file by absolute path
|
|
385
|
+
app.put("/api/open-file", async (req, res) => {
|
|
386
|
+
const filePath = req.query.path;
|
|
387
|
+
const { content, html } = req.body || {};
|
|
388
|
+
|
|
389
|
+
if (!filePath || typeof filePath !== "string") {
|
|
390
|
+
return res.status(400).json({ error: "Missing file path" });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let finalContent = content;
|
|
394
|
+
if (typeof html === "string") {
|
|
395
|
+
finalContent = turndownService.turndown(html);
|
|
396
|
+
}
|
|
397
|
+
if (typeof finalContent !== "string") {
|
|
398
|
+
return res.status(400).json({ error: "Missing content" });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const resolvedPath = path.resolve(filePath);
|
|
403
|
+
await fsp.writeFile(resolvedPath, finalContent, "utf-8");
|
|
404
|
+
const stats = await fsp.stat(resolvedPath);
|
|
405
|
+
const renderedHtml = marked.parse(finalContent);
|
|
406
|
+
|
|
407
|
+
res.json({
|
|
408
|
+
path: resolvedPath,
|
|
409
|
+
name: path.basename(resolvedPath),
|
|
410
|
+
content: finalContent,
|
|
411
|
+
html: renderedHtml,
|
|
412
|
+
metadata: {
|
|
413
|
+
size: stats.size,
|
|
414
|
+
modifiedAt: stats.mtime,
|
|
415
|
+
createdAt: stats.birthtime,
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
} catch (error) {
|
|
419
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to save file" });
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
300
423
|
app.use(express.static(path.join(__dirname, "..", "public")));
|
|
301
424
|
|
|
302
425
|
app.get("*", (_req, res) => {
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -912,9 +912,281 @@
|
|
|
912
912
|
align-items: flex-start;
|
|
913
913
|
}
|
|
914
914
|
}
|
|
915
|
+
|
|
916
|
+
/* File browser modal */
|
|
917
|
+
.modal-overlay {
|
|
918
|
+
display: none;
|
|
919
|
+
position: fixed;
|
|
920
|
+
inset: 0;
|
|
921
|
+
background: rgba(0, 0, 0, 0.5);
|
|
922
|
+
z-index: 200;
|
|
923
|
+
align-items: center;
|
|
924
|
+
justify-content: center;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
.modal-overlay.open {
|
|
928
|
+
display: flex;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
.modal {
|
|
932
|
+
background: var(--panel);
|
|
933
|
+
border-radius: var(--radius);
|
|
934
|
+
box-shadow: var(--shadow);
|
|
935
|
+
width: 90%;
|
|
936
|
+
max-width: 600px;
|
|
937
|
+
max-height: 80vh;
|
|
938
|
+
display: flex;
|
|
939
|
+
flex-direction: column;
|
|
940
|
+
overflow: hidden;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.modal-header {
|
|
944
|
+
display: flex;
|
|
945
|
+
align-items: center;
|
|
946
|
+
justify-content: space-between;
|
|
947
|
+
padding: 16px 20px;
|
|
948
|
+
border-bottom: 1px solid var(--border);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
.modal-title {
|
|
952
|
+
font-size: 16px;
|
|
953
|
+
font-weight: 600;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
.modal-close {
|
|
957
|
+
width: 32px;
|
|
958
|
+
height: 32px;
|
|
959
|
+
border: none;
|
|
960
|
+
background: transparent;
|
|
961
|
+
border-radius: 6px;
|
|
962
|
+
cursor: pointer;
|
|
963
|
+
color: var(--muted);
|
|
964
|
+
display: flex;
|
|
965
|
+
align-items: center;
|
|
966
|
+
justify-content: center;
|
|
967
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.modal-close:hover {
|
|
971
|
+
background: var(--hover-bg);
|
|
972
|
+
color: var(--ink);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
.modal-close svg {
|
|
976
|
+
width: 18px;
|
|
977
|
+
height: 18px;
|
|
978
|
+
stroke: currentColor;
|
|
979
|
+
stroke-width: 2;
|
|
980
|
+
fill: none;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.modal-body {
|
|
984
|
+
flex: 1;
|
|
985
|
+
overflow-y: auto;
|
|
986
|
+
padding: 16px 20px;
|
|
987
|
+
display: flex;
|
|
988
|
+
flex-direction: column;
|
|
989
|
+
gap: 16px;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.path-input-wrap {
|
|
993
|
+
display: flex;
|
|
994
|
+
gap: 8px;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.path-input {
|
|
998
|
+
flex: 1;
|
|
999
|
+
border: 1px solid var(--border);
|
|
1000
|
+
border-radius: 8px;
|
|
1001
|
+
padding: 10px 12px;
|
|
1002
|
+
font-size: 14px;
|
|
1003
|
+
background: var(--panel-strong);
|
|
1004
|
+
color: var(--ink);
|
|
1005
|
+
font-family: var(--font-mono);
|
|
1006
|
+
outline: none;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.path-input:focus {
|
|
1010
|
+
border-color: var(--accent);
|
|
1011
|
+
box-shadow: 0 0 0 2px rgba(47, 111, 237, 0.15);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.path-go-btn {
|
|
1015
|
+
padding: 10px 16px;
|
|
1016
|
+
border: 1px solid var(--accent);
|
|
1017
|
+
background: var(--accent);
|
|
1018
|
+
color: #fff;
|
|
1019
|
+
border-radius: 8px;
|
|
1020
|
+
cursor: pointer;
|
|
1021
|
+
font-size: 14px;
|
|
1022
|
+
font-family: var(--font-ui);
|
|
1023
|
+
transition: background 0.15s ease;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.path-go-btn:hover {
|
|
1027
|
+
background: var(--accent-strong);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
.browser-nav {
|
|
1031
|
+
display: flex;
|
|
1032
|
+
align-items: center;
|
|
1033
|
+
gap: 8px;
|
|
1034
|
+
padding: 8px 12px;
|
|
1035
|
+
background: var(--folder-bg);
|
|
1036
|
+
border-radius: 8px;
|
|
1037
|
+
border: 1px solid var(--border);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
.browser-back {
|
|
1041
|
+
width: 28px;
|
|
1042
|
+
height: 28px;
|
|
1043
|
+
border: none;
|
|
1044
|
+
background: transparent;
|
|
1045
|
+
border-radius: 6px;
|
|
1046
|
+
cursor: pointer;
|
|
1047
|
+
color: var(--muted);
|
|
1048
|
+
display: flex;
|
|
1049
|
+
align-items: center;
|
|
1050
|
+
justify-content: center;
|
|
1051
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.browser-back:hover {
|
|
1055
|
+
background: var(--hover-bg);
|
|
1056
|
+
color: var(--ink);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
.browser-back svg {
|
|
1060
|
+
width: 16px;
|
|
1061
|
+
height: 16px;
|
|
1062
|
+
stroke: currentColor;
|
|
1063
|
+
stroke-width: 2;
|
|
1064
|
+
fill: none;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.browser-path {
|
|
1068
|
+
flex: 1;
|
|
1069
|
+
font-size: 12px;
|
|
1070
|
+
color: var(--muted);
|
|
1071
|
+
font-family: var(--font-mono);
|
|
1072
|
+
overflow: hidden;
|
|
1073
|
+
text-overflow: ellipsis;
|
|
1074
|
+
white-space: nowrap;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
.browser-list {
|
|
1078
|
+
list-style: none;
|
|
1079
|
+
padding: 0;
|
|
1080
|
+
margin: 0;
|
|
1081
|
+
display: flex;
|
|
1082
|
+
flex-direction: column;
|
|
1083
|
+
gap: 2px;
|
|
1084
|
+
max-height: 300px;
|
|
1085
|
+
overflow-y: auto;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
.browser-item {
|
|
1089
|
+
display: flex;
|
|
1090
|
+
align-items: center;
|
|
1091
|
+
gap: 10px;
|
|
1092
|
+
padding: 10px 12px;
|
|
1093
|
+
border-radius: 8px;
|
|
1094
|
+
cursor: pointer;
|
|
1095
|
+
transition: background 0.15s ease;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
.browser-item:hover {
|
|
1099
|
+
background: var(--hover-bg);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.browser-item svg {
|
|
1103
|
+
width: 18px;
|
|
1104
|
+
height: 18px;
|
|
1105
|
+
stroke: currentColor;
|
|
1106
|
+
stroke-width: 2;
|
|
1107
|
+
fill: none;
|
|
1108
|
+
flex-shrink: 0;
|
|
1109
|
+
color: var(--muted);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
.browser-item.folder svg {
|
|
1113
|
+
color: var(--accent);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
.browser-item-name {
|
|
1117
|
+
flex: 1;
|
|
1118
|
+
font-size: 14px;
|
|
1119
|
+
overflow: hidden;
|
|
1120
|
+
text-overflow: ellipsis;
|
|
1121
|
+
white-space: nowrap;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
.browser-empty {
|
|
1125
|
+
padding: 20px;
|
|
1126
|
+
text-align: center;
|
|
1127
|
+
color: var(--muted);
|
|
1128
|
+
font-size: 14px;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.open-file-btn {
|
|
1132
|
+
display: flex;
|
|
1133
|
+
align-items: center;
|
|
1134
|
+
justify-content: center;
|
|
1135
|
+
gap: 6px;
|
|
1136
|
+
width: 100%;
|
|
1137
|
+
padding: 10px;
|
|
1138
|
+
border: 1px dashed var(--border);
|
|
1139
|
+
background: transparent;
|
|
1140
|
+
border-radius: 8px;
|
|
1141
|
+
cursor: pointer;
|
|
1142
|
+
font-size: 13px;
|
|
1143
|
+
color: var(--muted);
|
|
1144
|
+
font-family: var(--font-ui);
|
|
1145
|
+
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
|
1146
|
+
margin-bottom: 8px;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.open-file-btn:hover {
|
|
1150
|
+
background: var(--hover-bg);
|
|
1151
|
+
border-color: var(--accent);
|
|
1152
|
+
color: var(--accent);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
.open-file-btn svg {
|
|
1156
|
+
width: 16px;
|
|
1157
|
+
height: 16px;
|
|
1158
|
+
stroke: currentColor;
|
|
1159
|
+
stroke-width: 2;
|
|
1160
|
+
fill: none;
|
|
1161
|
+
}
|
|
915
1162
|
</style>
|
|
916
1163
|
</head>
|
|
917
1164
|
<body>
|
|
1165
|
+
<!-- File browser modal -->
|
|
1166
|
+
<div class="modal-overlay" id="file-modal">
|
|
1167
|
+
<div class="modal">
|
|
1168
|
+
<div class="modal-header">
|
|
1169
|
+
<div class="modal-title">Open File</div>
|
|
1170
|
+
<button class="modal-close" id="modal-close">
|
|
1171
|
+
<svg viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"></path></svg>
|
|
1172
|
+
</button>
|
|
1173
|
+
</div>
|
|
1174
|
+
<div class="modal-body">
|
|
1175
|
+
<div class="path-input-wrap">
|
|
1176
|
+
<input type="text" class="path-input" id="path-input" placeholder="Paste file path or type to search...">
|
|
1177
|
+
<button class="path-go-btn" id="path-go-btn">Open</button>
|
|
1178
|
+
</div>
|
|
1179
|
+
<div class="browser-nav">
|
|
1180
|
+
<button class="browser-back" id="browser-back" title="Go up">
|
|
1181
|
+
<svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
|
1182
|
+
</button>
|
|
1183
|
+
<div class="browser-path" id="browser-path">~</div>
|
|
1184
|
+
</div>
|
|
1185
|
+
<ul class="browser-list" id="browser-list"></ul>
|
|
1186
|
+
</div>
|
|
1187
|
+
</div>
|
|
1188
|
+
</div>
|
|
1189
|
+
|
|
918
1190
|
<div class="app">
|
|
919
1191
|
<aside class="sidebar">
|
|
920
1192
|
<div class="brand">
|
|
@@ -922,6 +1194,10 @@
|
|
|
922
1194
|
<div class="pill" id="file-count">0 files</div>
|
|
923
1195
|
</div>
|
|
924
1196
|
<div class="folder" id="folder"></div>
|
|
1197
|
+
<button class="open-file-btn" id="open-file-btn">
|
|
1198
|
+
<svg viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
|
1199
|
+
Open File...
|
|
1200
|
+
</button>
|
|
925
1201
|
<div class="search">
|
|
926
1202
|
<input class="filter" id="filter" placeholder="Filter files..." />
|
|
927
1203
|
<div class="search-hint">Cmd+K</div>
|
|
@@ -1057,6 +1333,8 @@
|
|
|
1057
1333
|
currentContent: "",
|
|
1058
1334
|
currentHtml: "",
|
|
1059
1335
|
tocHeadings: [],
|
|
1336
|
+
openedFile: null, // For files opened via file browser (absolute path)
|
|
1337
|
+
browserPath: "",
|
|
1060
1338
|
};
|
|
1061
1339
|
|
|
1062
1340
|
const fileListEl = document.getElementById("file-list");
|
|
@@ -1081,6 +1359,14 @@
|
|
|
1081
1359
|
const linkBtn = document.getElementById("link-btn");
|
|
1082
1360
|
const widthToggle = document.getElementById("width-toggle");
|
|
1083
1361
|
const focusBtn = document.getElementById("focus-btn");
|
|
1362
|
+
const openFileBtnEl = document.getElementById("open-file-btn");
|
|
1363
|
+
const fileModal = document.getElementById("file-modal");
|
|
1364
|
+
const modalCloseBtn = document.getElementById("modal-close");
|
|
1365
|
+
const pathInput = document.getElementById("path-input");
|
|
1366
|
+
const pathGoBtn = document.getElementById("path-go-btn");
|
|
1367
|
+
const browserBackBtn = document.getElementById("browser-back");
|
|
1368
|
+
const browserPathEl = document.getElementById("browser-path");
|
|
1369
|
+
const browserListEl = document.getElementById("browser-list");
|
|
1084
1370
|
|
|
1085
1371
|
let tocTimer = null;
|
|
1086
1372
|
let scrollRaf = null;
|
|
@@ -1168,6 +1454,7 @@
|
|
|
1168
1454
|
|
|
1169
1455
|
async function openFile(fileId) {
|
|
1170
1456
|
state.selectedId = fileId;
|
|
1457
|
+
state.openedFile = null; // Clear externally opened file
|
|
1171
1458
|
renderList();
|
|
1172
1459
|
setEditMode(false);
|
|
1173
1460
|
metaEl.textContent = "";
|
|
@@ -1246,6 +1533,11 @@
|
|
|
1246
1533
|
}
|
|
1247
1534
|
|
|
1248
1535
|
async function saveFile() {
|
|
1536
|
+
// Handle externally opened files
|
|
1537
|
+
if (state.openedFile) {
|
|
1538
|
+
return saveExternalFile();
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1249
1541
|
if (!state.selectedId) return;
|
|
1250
1542
|
const html = contentEl.innerHTML;
|
|
1251
1543
|
saveEl.disabled = true;
|
|
@@ -1618,11 +1910,194 @@
|
|
|
1618
1910
|
toggleFocusMode();
|
|
1619
1911
|
});
|
|
1620
1912
|
|
|
1621
|
-
// Escape key to exit focus mode
|
|
1913
|
+
// Escape key to exit focus mode or close modal
|
|
1622
1914
|
document.addEventListener('keydown', (event) => {
|
|
1623
|
-
if (event.key === 'Escape'
|
|
1624
|
-
|
|
1915
|
+
if (event.key === 'Escape') {
|
|
1916
|
+
if (fileModal.classList.contains('open')) {
|
|
1917
|
+
closeFileModal();
|
|
1918
|
+
} else if (document.body.classList.contains('focus-mode')) {
|
|
1919
|
+
document.body.classList.remove('focus-mode');
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
// File browser modal
|
|
1925
|
+
function openFileModal() {
|
|
1926
|
+
fileModal.classList.add('open');
|
|
1927
|
+
pathInput.value = '';
|
|
1928
|
+
pathInput.focus();
|
|
1929
|
+
browsePath(state.browserPath || '');
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function closeFileModal() {
|
|
1933
|
+
fileModal.classList.remove('open');
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
async function browsePath(dirPath) {
|
|
1937
|
+
try {
|
|
1938
|
+
const res = await fetch(`/api/browse?path=${encodeURIComponent(dirPath)}`);
|
|
1939
|
+
if (!res.ok) {
|
|
1940
|
+
const err = await res.json();
|
|
1941
|
+
console.error('Browse error:', err.error);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
const data = await res.json();
|
|
1945
|
+
state.browserPath = data.current;
|
|
1946
|
+
browserPathEl.textContent = data.current;
|
|
1947
|
+
renderBrowserList(data.items, data.parent);
|
|
1948
|
+
} catch (err) {
|
|
1949
|
+
console.error('Browse error:', err);
|
|
1625
1950
|
}
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function renderBrowserList(items, parentPath) {
|
|
1954
|
+
browserListEl.innerHTML = '';
|
|
1955
|
+
|
|
1956
|
+
if (items.length === 0) {
|
|
1957
|
+
const empty = document.createElement('li');
|
|
1958
|
+
empty.className = 'browser-empty';
|
|
1959
|
+
empty.textContent = 'No markdown files in this folder';
|
|
1960
|
+
browserListEl.appendChild(empty);
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
items.forEach(item => {
|
|
1965
|
+
const li = document.createElement('li');
|
|
1966
|
+
li.className = 'browser-item' + (item.isDirectory ? ' folder' : '');
|
|
1967
|
+
|
|
1968
|
+
const icon = item.isDirectory
|
|
1969
|
+
? '<svg viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>'
|
|
1970
|
+
: '<svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>';
|
|
1971
|
+
|
|
1972
|
+
li.innerHTML = `${icon}<span class="browser-item-name">${item.name}</span>`;
|
|
1973
|
+
|
|
1974
|
+
li.addEventListener('click', () => {
|
|
1975
|
+
if (item.isDirectory) {
|
|
1976
|
+
browsePath(item.path);
|
|
1977
|
+
} else {
|
|
1978
|
+
openExternalFile(item.path);
|
|
1979
|
+
closeFileModal();
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
browserListEl.appendChild(li);
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
async function openExternalFile(filePath) {
|
|
1988
|
+
try {
|
|
1989
|
+
const res = await fetch(`/api/open-file?path=${encodeURIComponent(filePath)}`);
|
|
1990
|
+
if (!res.ok) {
|
|
1991
|
+
alert('Failed to open file');
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
const data = await res.json();
|
|
1995
|
+
|
|
1996
|
+
// Clear selection from file list
|
|
1997
|
+
state.selectedId = null;
|
|
1998
|
+
state.openedFile = {
|
|
1999
|
+
path: data.path,
|
|
2000
|
+
name: data.name,
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
renderList();
|
|
2004
|
+
|
|
2005
|
+
state.currentContent = data.content || '';
|
|
2006
|
+
state.currentHtml = data.html || '';
|
|
2007
|
+
contentEl.classList.remove('empty');
|
|
2008
|
+
contentEl.innerHTML = state.currentHtml;
|
|
2009
|
+
editToggleEl.disabled = false;
|
|
2010
|
+
|
|
2011
|
+
// Update header
|
|
2012
|
+
const displayName = data.name.replace(/\.md$/i, '');
|
|
2013
|
+
docTitleEl.textContent = displayName || 'Untitled';
|
|
2014
|
+
docPathEl.textContent = data.path;
|
|
2015
|
+
document.title = displayName ? `${displayName} - skimmd` : 'skimmd';
|
|
2016
|
+
|
|
2017
|
+
if (data.metadata) {
|
|
2018
|
+
const updated = new Date(data.metadata.modifiedAt).toLocaleString();
|
|
2019
|
+
metaEl.textContent = `Last updated ${updated}`;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
buildToc(contentEl);
|
|
2023
|
+
setEditMode(false);
|
|
2024
|
+
} catch (err) {
|
|
2025
|
+
console.error('Open file error:', err);
|
|
2026
|
+
alert('Failed to open file');
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
async function saveExternalFile() {
|
|
2031
|
+
if (!state.openedFile) return;
|
|
2032
|
+
const html = contentEl.innerHTML;
|
|
2033
|
+
saveEl.disabled = true;
|
|
2034
|
+
try {
|
|
2035
|
+
const res = await fetch(`/api/open-file?path=${encodeURIComponent(state.openedFile.path)}`, {
|
|
2036
|
+
method: 'PUT',
|
|
2037
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2038
|
+
body: JSON.stringify({ html }),
|
|
2039
|
+
});
|
|
2040
|
+
if (!res.ok) {
|
|
2041
|
+
alert('Failed to save file');
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
const data = await res.json();
|
|
2045
|
+
state.currentContent = data.content || '';
|
|
2046
|
+
state.currentHtml = data.html || html;
|
|
2047
|
+
contentEl.innerHTML = state.currentHtml;
|
|
2048
|
+
if (data.metadata) {
|
|
2049
|
+
const updated = new Date(data.metadata.modifiedAt).toLocaleString();
|
|
2050
|
+
metaEl.textContent = `Last updated ${updated}`;
|
|
2051
|
+
}
|
|
2052
|
+
buildToc(contentEl);
|
|
2053
|
+
setEditMode(false);
|
|
2054
|
+
} finally {
|
|
2055
|
+
saveEl.disabled = false;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// Open file button
|
|
2060
|
+
openFileBtnEl.addEventListener('click', openFileModal);
|
|
2061
|
+
|
|
2062
|
+
// Modal close
|
|
2063
|
+
modalCloseBtn.addEventListener('click', closeFileModal);
|
|
2064
|
+
fileModal.addEventListener('click', (e) => {
|
|
2065
|
+
if (e.target === fileModal) closeFileModal();
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
// Path input
|
|
2069
|
+
pathInput.addEventListener('keydown', (e) => {
|
|
2070
|
+
if (e.key === 'Enter') {
|
|
2071
|
+
const value = pathInput.value.trim();
|
|
2072
|
+
if (value) {
|
|
2073
|
+
// Check if it looks like a file path
|
|
2074
|
+
if (value.endsWith('.md') || value.endsWith('.markdown') || value.endsWith('.txt')) {
|
|
2075
|
+
openExternalFile(value);
|
|
2076
|
+
closeFileModal();
|
|
2077
|
+
} else {
|
|
2078
|
+
browsePath(value);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
pathGoBtn.addEventListener('click', () => {
|
|
2085
|
+
const value = pathInput.value.trim();
|
|
2086
|
+
if (value) {
|
|
2087
|
+
if (value.endsWith('.md') || value.endsWith('.markdown') || value.endsWith('.txt')) {
|
|
2088
|
+
openExternalFile(value);
|
|
2089
|
+
closeFileModal();
|
|
2090
|
+
} else {
|
|
2091
|
+
browsePath(value);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
// Browser back button
|
|
2097
|
+
browserBackBtn.addEventListener('click', () => {
|
|
2098
|
+
const current = state.browserPath;
|
|
2099
|
+
const parent = current.split('/').slice(0, -1).join('/') || '/';
|
|
2100
|
+
browsePath(parent);
|
|
1626
2101
|
});
|
|
1627
2102
|
|
|
1628
2103
|
// Live reload via Server-Sent Events
|