@youtyan/code-viewer 0.1.23 → 0.1.25
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/dist/code-viewer.js +92 -9
- package/package.json +1 -1
- package/web/app.js +221 -60
- package/web/style.css +86 -2
package/dist/code-viewer.js
CHANGED
|
@@ -313,6 +313,13 @@ function objectByteSize(oid, cwd) {
|
|
|
313
313
|
stderr: res.stderr
|
|
314
314
|
};
|
|
315
315
|
}
|
|
316
|
+
function lastCommitDateForPath(ref, path, cwd) {
|
|
317
|
+
const args = ["git", "log", "-1", "--format=%cI", ref, "--", path];
|
|
318
|
+
const res = run(args, cwd);
|
|
319
|
+
if (res.code !== 0)
|
|
320
|
+
return null;
|
|
321
|
+
return res.stdout.trim() || null;
|
|
322
|
+
}
|
|
316
323
|
function objectId(ref, path, cwd) {
|
|
317
324
|
const res = run(["git", "rev-parse", "--verify", `${ref}:${path}`], cwd);
|
|
318
325
|
const oid = res.stdout.trim();
|
|
@@ -1694,6 +1701,71 @@ function parentRepoPath(path) {
|
|
|
1694
1701
|
const parent = dirname2(path);
|
|
1695
1702
|
return parent === "." ? "" : parent;
|
|
1696
1703
|
}
|
|
1704
|
+
function isoDate(ms) {
|
|
1705
|
+
return ms && Number.isFinite(ms) ? new Date(ms).toISOString() : undefined;
|
|
1706
|
+
}
|
|
1707
|
+
function worktreeFileMetadata(path, knownSize) {
|
|
1708
|
+
const full = safeWorktreePath(path);
|
|
1709
|
+
if (!full)
|
|
1710
|
+
return {};
|
|
1711
|
+
try {
|
|
1712
|
+
const stat = statSync(full);
|
|
1713
|
+
return {
|
|
1714
|
+
size: knownSize ?? stat.size,
|
|
1715
|
+
created_at: isoDate(stat.birthtimeMs),
|
|
1716
|
+
updated_at: isoDate(stat.mtimeMs)
|
|
1717
|
+
};
|
|
1718
|
+
} catch {
|
|
1719
|
+
return {};
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
function gitFileMetadata(ref, path, knownSize) {
|
|
1723
|
+
const size = knownSize ?? rawFileSize(path, ref);
|
|
1724
|
+
const commitUpdatedAt = lastCommitDateForPath(ref, path, cwd) || undefined;
|
|
1725
|
+
return {
|
|
1726
|
+
size: size == null ? undefined : size,
|
|
1727
|
+
updated_at: commitUpdatedAt,
|
|
1728
|
+
commit_updated_at: commitUpdatedAt
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
function directoryMetadata(target, path) {
|
|
1732
|
+
if (target === "worktree" || target === "") {
|
|
1733
|
+
const full = path === "" ? safeOpenWorktreePath("") : safeWorktreePath(path);
|
|
1734
|
+
if (!full)
|
|
1735
|
+
return {};
|
|
1736
|
+
try {
|
|
1737
|
+
const stat = statSync(full);
|
|
1738
|
+
return {
|
|
1739
|
+
created_at: isoDate(stat.birthtimeMs),
|
|
1740
|
+
updated_at: isoDate(stat.mtimeMs)
|
|
1741
|
+
};
|
|
1742
|
+
} catch {
|
|
1743
|
+
return {};
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
const commitUpdatedAt = lastCommitDateForPath(target, path || ".", cwd) || undefined;
|
|
1747
|
+
return { updated_at: commitUpdatedAt, commit_updated_at: commitUpdatedAt };
|
|
1748
|
+
}
|
|
1749
|
+
function fileMetadataForTarget(target, path) {
|
|
1750
|
+
return target === "worktree" || target === "" ? worktreeFileMetadata(path) : gitFileMetadata(target, path);
|
|
1751
|
+
}
|
|
1752
|
+
function attachTreeEntryMetadata(target, entry) {
|
|
1753
|
+
if (entry.type === "tree")
|
|
1754
|
+
return { ...entry, ...directoryMetadata(target, entry.path) };
|
|
1755
|
+
if (entry.type !== "blob")
|
|
1756
|
+
return entry;
|
|
1757
|
+
return { ...entry, ...fileMetadataForTarget(target, entry.path) };
|
|
1758
|
+
}
|
|
1759
|
+
function fileMetadataHeaders(metadata) {
|
|
1760
|
+
const headers = {};
|
|
1761
|
+
if (metadata.created_at)
|
|
1762
|
+
headers["X-Code-Viewer-Created-At"] = metadata.created_at;
|
|
1763
|
+
if (metadata.updated_at)
|
|
1764
|
+
headers["X-Code-Viewer-Updated-At"] = metadata.updated_at;
|
|
1765
|
+
if (metadata.commit_updated_at)
|
|
1766
|
+
headers["X-Code-Viewer-Commit-Updated-At"] = metadata.commit_updated_at;
|
|
1767
|
+
return headers;
|
|
1768
|
+
}
|
|
1697
1769
|
function readReadme(target, dirPath) {
|
|
1698
1770
|
const candidates = ["README.md", "readme.md", "README.markdown", "README"];
|
|
1699
1771
|
for (const name of candidates) {
|
|
@@ -1735,7 +1807,7 @@ function handleTree(url) {
|
|
|
1735
1807
|
path,
|
|
1736
1808
|
project: basename2(cwd),
|
|
1737
1809
|
branch: currentBranch(cwd) || undefined,
|
|
1738
|
-
entries,
|
|
1810
|
+
entries: recursive ? entries : entries.map((entry) => attachTreeEntryMetadata(target, entry)),
|
|
1739
1811
|
readme: readReadme(target, path),
|
|
1740
1812
|
upload_enabled: allowUpload && (target === "worktree" || target === "")
|
|
1741
1813
|
});
|
|
@@ -2169,13 +2241,18 @@ function handleRawFile(req, url) {
|
|
|
2169
2241
|
const size = rawFileSize(path, ref);
|
|
2170
2242
|
if (size == null)
|
|
2171
2243
|
return text("not in ref", 404);
|
|
2244
|
+
const metadata = gitFileMetadata(ref, path, size);
|
|
2172
2245
|
if (req.method === "HEAD")
|
|
2173
|
-
return new Response(null, {
|
|
2246
|
+
return new Response(null, {
|
|
2247
|
+
headers: rawFileHeaders(path, size, undefined, metadata)
|
|
2248
|
+
});
|
|
2174
2249
|
const res = showBytes(ref, path, cwd);
|
|
2175
2250
|
if (res.code !== 0)
|
|
2176
2251
|
return text("not in ref", 404);
|
|
2177
2252
|
body = res.stdout.buffer.slice(res.stdout.byteOffset, res.stdout.byteOffset + res.stdout.byteLength);
|
|
2178
|
-
return new Response(body, {
|
|
2253
|
+
return new Response(body, {
|
|
2254
|
+
headers: rawFileHeaders(path, size, undefined, metadata)
|
|
2255
|
+
});
|
|
2179
2256
|
} else {
|
|
2180
2257
|
const full = safeWorktreePath(path);
|
|
2181
2258
|
if (!full)
|
|
@@ -2183,12 +2260,13 @@ function handleRawFile(req, url) {
|
|
|
2183
2260
|
const size = rawFileSize(path, ref);
|
|
2184
2261
|
if (size == null)
|
|
2185
2262
|
return text("not found", 404);
|
|
2263
|
+
const metadata = worktreeFileMetadata(path, size);
|
|
2186
2264
|
const rangeResult = req.headers.get("range") ? parseHttpByteRange(req.headers.get("range"), size) : null;
|
|
2187
2265
|
if (rangeResult?.kind === "unsatisfiable") {
|
|
2188
2266
|
return new Response(null, {
|
|
2189
2267
|
status: 416,
|
|
2190
2268
|
headers: {
|
|
2191
|
-
...rawFileHeaders(path, size),
|
|
2269
|
+
...rawFileHeaders(path, size, undefined, metadata),
|
|
2192
2270
|
"Content-Range": `bytes */${size}`,
|
|
2193
2271
|
"Content-Length": "0"
|
|
2194
2272
|
}
|
|
@@ -2199,18 +2277,20 @@ function handleRawFile(req, url) {
|
|
|
2199
2277
|
if (req.method === "HEAD") {
|
|
2200
2278
|
return new Response(null, {
|
|
2201
2279
|
status: 206,
|
|
2202
|
-
headers: rawFileHeaders(path, size, range)
|
|
2280
|
+
headers: rawFileHeaders(path, size, range, metadata)
|
|
2203
2281
|
});
|
|
2204
2282
|
}
|
|
2205
2283
|
return new Response(fileByteRangeResponseBody(full, range.start, range.end), {
|
|
2206
2284
|
status: 206,
|
|
2207
|
-
headers: rawFileHeaders(path, size, range)
|
|
2285
|
+
headers: rawFileHeaders(path, size, range, metadata)
|
|
2208
2286
|
});
|
|
2209
2287
|
}
|
|
2210
2288
|
if (req.method === "HEAD")
|
|
2211
|
-
return new Response(null, {
|
|
2289
|
+
return new Response(null, {
|
|
2290
|
+
headers: rawFileHeaders(path, size, undefined, metadata)
|
|
2291
|
+
});
|
|
2212
2292
|
return new Response(fileReadableStream(full), {
|
|
2213
|
-
headers: rawFileHeaders(path, size)
|
|
2293
|
+
headers: rawFileHeaders(path, size, undefined, metadata)
|
|
2214
2294
|
});
|
|
2215
2295
|
}
|
|
2216
2296
|
}
|
|
@@ -2230,7 +2310,7 @@ function rawFileSize(path, ref) {
|
|
|
2230
2310
|
return null;
|
|
2231
2311
|
}
|
|
2232
2312
|
}
|
|
2233
|
-
function rawFileHeaders(path, size = null, range) {
|
|
2313
|
+
function rawFileHeaders(path, size = null, range, metadata = {}) {
|
|
2234
2314
|
const mime = {
|
|
2235
2315
|
".png": "image/png",
|
|
2236
2316
|
".jpg": "image/jpeg",
|
|
@@ -2263,6 +2343,9 @@ function rawFileHeaders(path, size = null, range) {
|
|
|
2263
2343
|
} else if (size != null) {
|
|
2264
2344
|
headers["Content-Length"] = String(size);
|
|
2265
2345
|
}
|
|
2346
|
+
for (const [key, value] of Object.entries(fileMetadataHeaders(metadata))) {
|
|
2347
|
+
headers[key] = value;
|
|
2348
|
+
}
|
|
2266
2349
|
return headers;
|
|
2267
2350
|
}
|
|
2268
2351
|
function isForbiddenUploadName(name) {
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -6942,6 +6942,10 @@ ${frontmatter.yaml}
|
|
|
6942
6942
|
let SIDEBAR_ROW_BY_PATH = new Map;
|
|
6943
6943
|
let SIDEBAR_VIRTUAL_ACTIVE_PATH = "";
|
|
6944
6944
|
const SIDEBAR_TREE_ITEMS_CACHE = new WeakMap;
|
|
6945
|
+
let REPO_SORT = {
|
|
6946
|
+
key: "name",
|
|
6947
|
+
direction: "asc"
|
|
6948
|
+
};
|
|
6945
6949
|
let SERVER_SCOPE_OMIT_DIRS_DEFAULT = [];
|
|
6946
6950
|
let PENDING_G_SCOPE = null;
|
|
6947
6951
|
let PENDING_G_UNTIL = 0;
|
|
@@ -9130,69 +9134,81 @@ ${frontmatter.yaml}
|
|
|
9130
9134
|
if (meta.upload_enabled && (meta.ref === "worktree" || meta.ref === "")) {
|
|
9131
9135
|
listWrapper.appendChild(createRepoUploadPanel(meta.path || ""));
|
|
9132
9136
|
}
|
|
9137
|
+
const sortHost = document.createElement("div");
|
|
9138
|
+
sortHost.className = "gdp-repo-sort-host";
|
|
9133
9139
|
const list2 = document.createElement("div");
|
|
9134
9140
|
list2.className = "gdp-source-viewer gdp-repo-file-list";
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
const icon = document.createElement("span");
|
|
9161
|
-
icon.className = entry.type === "tree" ? "dir-icon" : "d2h-icon-wrapper";
|
|
9162
|
-
if (entry.type === "tree")
|
|
9163
|
-
setFolderIcon(icon, true);
|
|
9164
|
-
else
|
|
9165
|
-
icon.innerHTML = fileEntryIcon();
|
|
9166
|
-
const name = document.createElement("span");
|
|
9167
|
-
name.className = "name";
|
|
9168
|
-
name.textContent = entry.name;
|
|
9169
|
-
const kind = document.createElement("span");
|
|
9170
|
-
kind.className = "kind";
|
|
9171
|
-
kind.textContent = entry.type === "tree" ? "directory" : entry.type === "commit" ? "submodule" : "file";
|
|
9172
|
-
row.append(icon, name, kind);
|
|
9173
|
-
row.addEventListener("click", () => {
|
|
9174
|
-
if (entry.type === "tree") {
|
|
9175
|
-
setRoute(repoRoute(meta.ref, entry.path));
|
|
9141
|
+
const renderRepoRows = (focusSortKey) => {
|
|
9142
|
+
sortHost.replaceChildren(createRepoSortHeader(renderRepoRows));
|
|
9143
|
+
if (focusSortKey) {
|
|
9144
|
+
sortHost.querySelector(`[data-repo-sort="${focusSortKey}"]`)?.focus();
|
|
9145
|
+
}
|
|
9146
|
+
list2.replaceChildren();
|
|
9147
|
+
if (meta.path) {
|
|
9148
|
+
const parent = meta.path.split("/").slice(0, -1).join("/");
|
|
9149
|
+
const row = document.createElement("button");
|
|
9150
|
+
row.type = "button";
|
|
9151
|
+
row.className = "gdp-repo-row parent";
|
|
9152
|
+
const parentIcon = document.createElement("span");
|
|
9153
|
+
parentIcon.className = "dir-icon";
|
|
9154
|
+
setFolderIcon(parentIcon, false);
|
|
9155
|
+
const parentName = document.createElement("span");
|
|
9156
|
+
parentName.className = "name";
|
|
9157
|
+
parentName.textContent = "..";
|
|
9158
|
+
const parentKind = document.createElement("span");
|
|
9159
|
+
parentKind.className = "meta";
|
|
9160
|
+
parentKind.textContent = "";
|
|
9161
|
+
const parentSize = document.createElement("span");
|
|
9162
|
+
parentSize.className = "size";
|
|
9163
|
+
row.append(parentIcon, parentName, parentKind, parentSize);
|
|
9164
|
+
row.addEventListener("click", () => {
|
|
9165
|
+
setRoute(repoRoute(meta.ref, parent));
|
|
9176
9166
|
loadRepo();
|
|
9177
|
-
}
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9167
|
+
});
|
|
9168
|
+
list2.appendChild(row);
|
|
9169
|
+
}
|
|
9170
|
+
sortedRepoEntries(meta.entries).forEach((entry) => {
|
|
9171
|
+
const row = document.createElement("button");
|
|
9172
|
+
row.type = "button";
|
|
9173
|
+
row.className = `gdp-repo-row ${entry.type}`;
|
|
9174
|
+
const icon = document.createElement("span");
|
|
9175
|
+
icon.className = entry.type === "tree" ? "dir-icon" : "d2h-icon-wrapper";
|
|
9176
|
+
if (entry.type === "tree")
|
|
9177
|
+
setFolderIcon(icon, true);
|
|
9178
|
+
else
|
|
9179
|
+
icon.innerHTML = fileEntryIcon();
|
|
9180
|
+
const name = document.createElement("span");
|
|
9181
|
+
name.className = "name";
|
|
9182
|
+
name.textContent = entry.name;
|
|
9183
|
+
const metaBlock = createRepoEntryMeta(entry);
|
|
9184
|
+
const size = createRepoEntrySize(entry);
|
|
9185
|
+
row.append(icon, name, metaBlock, size);
|
|
9186
|
+
row.addEventListener("click", () => {
|
|
9187
|
+
if (entry.type === "tree") {
|
|
9188
|
+
setRoute(repoRoute(meta.ref, entry.path));
|
|
9189
|
+
loadRepo();
|
|
9190
|
+
} else if (entry.type === "blob") {
|
|
9191
|
+
setRoute({
|
|
9192
|
+
screen: "file",
|
|
9193
|
+
path: entry.path,
|
|
9194
|
+
ref: meta.ref,
|
|
9195
|
+
view: "blob",
|
|
9196
|
+
range: currentRange()
|
|
9197
|
+
});
|
|
9198
|
+
renderStandaloneSource({ path: entry.path, ref: meta.ref });
|
|
9199
|
+
}
|
|
9200
|
+
});
|
|
9201
|
+
list2.appendChild(row);
|
|
9187
9202
|
});
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9203
|
+
if (!meta.entries.length) {
|
|
9204
|
+
const empty = document.createElement("div");
|
|
9205
|
+
empty.className = "gdp-repo-empty";
|
|
9206
|
+
empty.textContent = "No files in this directory.";
|
|
9207
|
+
list2.appendChild(empty);
|
|
9208
|
+
}
|
|
9209
|
+
};
|
|
9210
|
+
listWrapper.appendChild(sortHost);
|
|
9211
|
+
renderRepoRows();
|
|
9196
9212
|
listWrapper.appendChild(list2);
|
|
9197
9213
|
listCard.appendChild(listWrapper);
|
|
9198
9214
|
shell.appendChild(listCard);
|
|
@@ -10335,6 +10351,117 @@ ${frontmatter.yaml}
|
|
|
10335
10351
|
}
|
|
10336
10352
|
return (unit === 0 ? String(value) : value.toFixed(value >= 10 ? 1 : 2).replace(/\.0+$/, "")) + " " + units[unit];
|
|
10337
10353
|
}
|
|
10354
|
+
function formatFileDate(value) {
|
|
10355
|
+
if (!value)
|
|
10356
|
+
return "";
|
|
10357
|
+
const date = new Date(value);
|
|
10358
|
+
if (Number.isNaN(date.getTime()))
|
|
10359
|
+
return "";
|
|
10360
|
+
return date.toLocaleString(undefined, {
|
|
10361
|
+
year: "numeric",
|
|
10362
|
+
month: "short",
|
|
10363
|
+
day: "numeric",
|
|
10364
|
+
hour: "2-digit",
|
|
10365
|
+
minute: "2-digit"
|
|
10366
|
+
});
|
|
10367
|
+
}
|
|
10368
|
+
function createRepoEntryMeta(entry) {
|
|
10369
|
+
const meta = document.createElement("span");
|
|
10370
|
+
meta.className = "meta";
|
|
10371
|
+
const updated = formatFileDate(entry.updated_at || entry.commit_updated_at);
|
|
10372
|
+
const created = formatFileDate(entry.created_at);
|
|
10373
|
+
if (entry.type === "tree" && updated) {
|
|
10374
|
+
meta.textContent = updated;
|
|
10375
|
+
if (created)
|
|
10376
|
+
meta.title = `Created ${created}`;
|
|
10377
|
+
return meta;
|
|
10378
|
+
}
|
|
10379
|
+
if (entry.type !== "blob") {
|
|
10380
|
+
meta.textContent = "-";
|
|
10381
|
+
return meta;
|
|
10382
|
+
}
|
|
10383
|
+
meta.textContent = updated ? updated : created ? created : "-";
|
|
10384
|
+
if (created)
|
|
10385
|
+
meta.title = `Created ${created}`;
|
|
10386
|
+
return meta;
|
|
10387
|
+
}
|
|
10388
|
+
function createRepoEntrySize(entry) {
|
|
10389
|
+
const size = document.createElement("span");
|
|
10390
|
+
size.className = "size";
|
|
10391
|
+
size.textContent = entry.type === "blob" && entry.size != null ? formatBytes(entry.size) : "";
|
|
10392
|
+
return size;
|
|
10393
|
+
}
|
|
10394
|
+
function repoEntryUpdatedTime(entry) {
|
|
10395
|
+
const raw = entry.updated_at || entry.commit_updated_at || entry.created_at;
|
|
10396
|
+
if (!raw)
|
|
10397
|
+
return -1;
|
|
10398
|
+
const time = new Date(raw).getTime();
|
|
10399
|
+
return Number.isNaN(time) ? -1 : time;
|
|
10400
|
+
}
|
|
10401
|
+
function sortedRepoEntries(entries) {
|
|
10402
|
+
const direction = REPO_SORT.direction === "asc" ? 1 : -1;
|
|
10403
|
+
return [...entries].sort((a2, b2) => {
|
|
10404
|
+
if (REPO_SORT.key === "name" && a2.type !== b2.type) {
|
|
10405
|
+
if (a2.type === "tree")
|
|
10406
|
+
return -1;
|
|
10407
|
+
if (b2.type === "tree")
|
|
10408
|
+
return 1;
|
|
10409
|
+
}
|
|
10410
|
+
let result = 0;
|
|
10411
|
+
if (REPO_SORT.key === "updated") {
|
|
10412
|
+
const aTime = repoEntryUpdatedTime(a2);
|
|
10413
|
+
const bTime = repoEntryUpdatedTime(b2);
|
|
10414
|
+
if (aTime < 0 && bTime >= 0)
|
|
10415
|
+
return 1;
|
|
10416
|
+
if (bTime < 0 && aTime >= 0)
|
|
10417
|
+
return -1;
|
|
10418
|
+
result = aTime - bTime;
|
|
10419
|
+
} else if (REPO_SORT.key === "size") {
|
|
10420
|
+
if (a2.size == null && b2.size != null)
|
|
10421
|
+
return 1;
|
|
10422
|
+
if (b2.size == null && a2.size != null)
|
|
10423
|
+
return -1;
|
|
10424
|
+
result = (a2.size ?? 0) - (b2.size ?? 0);
|
|
10425
|
+
} else {
|
|
10426
|
+
result = a2.name.localeCompare(b2.name);
|
|
10427
|
+
}
|
|
10428
|
+
if (result === 0)
|
|
10429
|
+
result = a2.name.localeCompare(b2.name);
|
|
10430
|
+
return result * direction;
|
|
10431
|
+
});
|
|
10432
|
+
}
|
|
10433
|
+
function createRepoSortHeader(onSortChange) {
|
|
10434
|
+
const header = document.createElement("div");
|
|
10435
|
+
header.className = "gdp-repo-sort-header";
|
|
10436
|
+
const spacer = document.createElement("span");
|
|
10437
|
+
spacer.className = "gdp-repo-sort-spacer";
|
|
10438
|
+
header.appendChild(spacer);
|
|
10439
|
+
const columns = [
|
|
10440
|
+
{ key: "name", label: "Name" },
|
|
10441
|
+
{ key: "updated", label: "Updated" },
|
|
10442
|
+
{ key: "size", label: "Size" }
|
|
10443
|
+
];
|
|
10444
|
+
columns.forEach((column) => {
|
|
10445
|
+
const button = document.createElement("button");
|
|
10446
|
+
button.type = "button";
|
|
10447
|
+
button.dataset.repoSort = column.key;
|
|
10448
|
+
button.textContent = column.label + (REPO_SORT.key === column.key ? REPO_SORT.direction === "asc" ? " ↑" : " ↓" : "");
|
|
10449
|
+
button.className = REPO_SORT.key === column.key ? "active" : "";
|
|
10450
|
+
button.addEventListener("click", () => {
|
|
10451
|
+
if (REPO_SORT.key === column.key) {
|
|
10452
|
+
REPO_SORT.direction = REPO_SORT.direction === "asc" ? "desc" : "asc";
|
|
10453
|
+
} else {
|
|
10454
|
+
REPO_SORT = {
|
|
10455
|
+
key: column.key,
|
|
10456
|
+
direction: column.key === "name" ? "asc" : "desc"
|
|
10457
|
+
};
|
|
10458
|
+
}
|
|
10459
|
+
onSortChange(column.key);
|
|
10460
|
+
});
|
|
10461
|
+
header.appendChild(button);
|
|
10462
|
+
});
|
|
10463
|
+
return header;
|
|
10464
|
+
}
|
|
10338
10465
|
function humanFileKind(path, mime, fallback) {
|
|
10339
10466
|
const ext = (path.split(".").pop() || "").toLowerCase();
|
|
10340
10467
|
if (ext === "png")
|
|
@@ -10392,12 +10519,41 @@ ${frontmatter.yaml}
|
|
|
10392
10519
|
const size = rawSize == null ? NaN : Number(rawSize);
|
|
10393
10520
|
return {
|
|
10394
10521
|
size: rawSize != null && Number.isFinite(size) ? size : undefined,
|
|
10395
|
-
type: res.headers.get("content-type") || undefined
|
|
10522
|
+
type: res.headers.get("content-type") || undefined,
|
|
10523
|
+
created_at: res.headers.get("x-code-viewer-created-at") || undefined,
|
|
10524
|
+
updated_at: res.headers.get("x-code-viewer-updated-at") || undefined,
|
|
10525
|
+
commit_updated_at: res.headers.get("x-code-viewer-commit-updated-at") || undefined
|
|
10396
10526
|
};
|
|
10397
10527
|
} catch {
|
|
10398
10528
|
return {};
|
|
10399
10529
|
}
|
|
10400
10530
|
}
|
|
10531
|
+
function createFileDetailMeta(target, meta) {
|
|
10532
|
+
const wrap = document.createElement("div");
|
|
10533
|
+
wrap.className = "gdp-file-detail-meta";
|
|
10534
|
+
const addItem = (label, value) => {
|
|
10535
|
+
if (!value)
|
|
10536
|
+
return;
|
|
10537
|
+
const item = document.createElement("span");
|
|
10538
|
+
item.className = "gdp-file-detail-meta-item";
|
|
10539
|
+
const labelEl = document.createElement("span");
|
|
10540
|
+
labelEl.className = "label";
|
|
10541
|
+
labelEl.textContent = label;
|
|
10542
|
+
const valueEl = document.createElement("span");
|
|
10543
|
+
valueEl.className = "value";
|
|
10544
|
+
valueEl.textContent = value;
|
|
10545
|
+
item.append(labelEl, valueEl);
|
|
10546
|
+
wrap.appendChild(item);
|
|
10547
|
+
};
|
|
10548
|
+
addItem("Size", meta.size == null ? "" : formatBytes(meta.size));
|
|
10549
|
+
addItem("Updated", formatFileDate(meta.updated_at || meta.commit_updated_at));
|
|
10550
|
+
addItem("Created", formatFileDate(meta.created_at));
|
|
10551
|
+
if (!wrap.childElementCount) {
|
|
10552
|
+
wrap.hidden = true;
|
|
10553
|
+
wrap.dataset.path = target.path;
|
|
10554
|
+
}
|
|
10555
|
+
return wrap;
|
|
10556
|
+
}
|
|
10401
10557
|
function createSourceFileInfo(target, kind) {
|
|
10402
10558
|
const info = document.createElement("div");
|
|
10403
10559
|
info.className = "gdp-source-file-info";
|
|
@@ -11423,6 +11579,11 @@ ${frontmatter.yaml}
|
|
|
11423
11579
|
name.appendChild(copy);
|
|
11424
11580
|
name.appendChild(createOpenPathButton(target.path, "file-parent", "open parent folder in OS"));
|
|
11425
11581
|
header.appendChild(name);
|
|
11582
|
+
loadRawFileInfo(target).then((meta) => {
|
|
11583
|
+
if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
|
|
11584
|
+
return;
|
|
11585
|
+
header.appendChild(createFileDetailMeta(target, meta));
|
|
11586
|
+
});
|
|
11426
11587
|
if (!repoTarget) {
|
|
11427
11588
|
const back = document.createElement("button");
|
|
11428
11589
|
back.type = "button";
|
package/web/style.css
CHANGED
|
@@ -2515,6 +2515,7 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
|
|
|
2515
2515
|
.gdp-file-detail-header {
|
|
2516
2516
|
display: flex;
|
|
2517
2517
|
align-items: center;
|
|
2518
|
+
flex-wrap: wrap;
|
|
2518
2519
|
gap: 12px;
|
|
2519
2520
|
min-height: 34px;
|
|
2520
2521
|
margin-bottom: 0;
|
|
@@ -2589,6 +2590,39 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
|
|
|
2589
2590
|
flex-shrink: 0;
|
|
2590
2591
|
color: var(--fg-muted);
|
|
2591
2592
|
}
|
|
2593
|
+
.gdp-file-detail-meta {
|
|
2594
|
+
display: flex;
|
|
2595
|
+
align-items: center;
|
|
2596
|
+
flex-wrap: wrap;
|
|
2597
|
+
gap: 0;
|
|
2598
|
+
color: var(--fg-muted);
|
|
2599
|
+
font-size: 12px;
|
|
2600
|
+
line-height: 18px;
|
|
2601
|
+
}
|
|
2602
|
+
.gdp-file-detail-meta-item {
|
|
2603
|
+
display: inline-flex;
|
|
2604
|
+
align-items: baseline;
|
|
2605
|
+
gap: 6px;
|
|
2606
|
+
padding: 0 10px;
|
|
2607
|
+
border-right: 1px solid var(--border-muted);
|
|
2608
|
+
white-space: nowrap;
|
|
2609
|
+
}
|
|
2610
|
+
.gdp-file-detail-meta-item:first-child {
|
|
2611
|
+
padding-left: 0;
|
|
2612
|
+
}
|
|
2613
|
+
.gdp-file-detail-meta-item:last-child {
|
|
2614
|
+
border-right: 0;
|
|
2615
|
+
padding-right: 0;
|
|
2616
|
+
}
|
|
2617
|
+
.gdp-file-detail-meta-item .label {
|
|
2618
|
+
font-size: 11px;
|
|
2619
|
+
font-weight: 600;
|
|
2620
|
+
color: var(--fg-subtle);
|
|
2621
|
+
}
|
|
2622
|
+
.gdp-file-detail-meta-item .value {
|
|
2623
|
+
color: var(--fg);
|
|
2624
|
+
font-weight: 500;
|
|
2625
|
+
}
|
|
2592
2626
|
.gdp-markdown-preview {
|
|
2593
2627
|
width: 100%;
|
|
2594
2628
|
max-width: none;
|
|
@@ -2998,7 +3032,7 @@ body.gdp-file-detail-page.gdp-repo-blob-page .sb-head {
|
|
|
2998
3032
|
justify-content: stretch;
|
|
2999
3033
|
align-items: center;
|
|
3000
3034
|
gap: 8px;
|
|
3001
|
-
top:
|
|
3035
|
+
top: 0;
|
|
3002
3036
|
z-index: 5;
|
|
3003
3037
|
}
|
|
3004
3038
|
body.gdp-repo-page .sb-head > #sidebar-toggle,
|
|
@@ -3124,10 +3158,46 @@ body.gdp-file-detail-page #empty {
|
|
|
3124
3158
|
padding: 0;
|
|
3125
3159
|
overflow: hidden;
|
|
3126
3160
|
}
|
|
3161
|
+
.gdp-repo-sort-header {
|
|
3162
|
+
display: grid;
|
|
3163
|
+
grid-template-columns: 28px minmax(0, 1fr) minmax(150px, 220px) 80px;
|
|
3164
|
+
column-gap: 8px;
|
|
3165
|
+
align-items: center;
|
|
3166
|
+
min-height: 34px;
|
|
3167
|
+
padding: 0 14px;
|
|
3168
|
+
border-top: 1px solid var(--border-muted);
|
|
3169
|
+
border-bottom: 1px solid var(--border-muted);
|
|
3170
|
+
background: var(--bg-soft);
|
|
3171
|
+
}
|
|
3172
|
+
.gdp-repo-sort-spacer {
|
|
3173
|
+
display: block;
|
|
3174
|
+
}
|
|
3175
|
+
.gdp-repo-sort-header button {
|
|
3176
|
+
appearance: none;
|
|
3177
|
+
height: 100%;
|
|
3178
|
+
border: 0;
|
|
3179
|
+
background: transparent;
|
|
3180
|
+
color: var(--fg-muted);
|
|
3181
|
+
font: inherit;
|
|
3182
|
+
font-size: 12px;
|
|
3183
|
+
font-weight: 600;
|
|
3184
|
+
text-align: left;
|
|
3185
|
+
cursor: pointer;
|
|
3186
|
+
line-height: 20px;
|
|
3187
|
+
padding: 0;
|
|
3188
|
+
}
|
|
3189
|
+
.gdp-repo-sort-header button:hover,
|
|
3190
|
+
.gdp-repo-sort-header button.active {
|
|
3191
|
+
color: var(--fg);
|
|
3192
|
+
}
|
|
3193
|
+
.gdp-repo-sort-header button[data-repo-sort="size"] {
|
|
3194
|
+
text-align: right;
|
|
3195
|
+
}
|
|
3127
3196
|
.gdp-repo-row {
|
|
3128
3197
|
display: grid;
|
|
3129
|
-
grid-template-columns: 28px minmax(0, 1fr)
|
|
3198
|
+
grid-template-columns: 28px minmax(0, 1fr) minmax(150px, 220px) 80px;
|
|
3130
3199
|
align-items: center;
|
|
3200
|
+
column-gap: 8px;
|
|
3131
3201
|
width: 100%;
|
|
3132
3202
|
min-height: 42px;
|
|
3133
3203
|
border: 0;
|
|
@@ -3173,6 +3243,20 @@ body.gdp-file-detail-page #empty {
|
|
|
3173
3243
|
color: var(--fg-muted);
|
|
3174
3244
|
font-size: 12px;
|
|
3175
3245
|
}
|
|
3246
|
+
.gdp-repo-row .meta {
|
|
3247
|
+
overflow: hidden;
|
|
3248
|
+
text-overflow: ellipsis;
|
|
3249
|
+
white-space: nowrap;
|
|
3250
|
+
color: var(--fg-muted);
|
|
3251
|
+
font-size: 12px;
|
|
3252
|
+
}
|
|
3253
|
+
.gdp-repo-row .size {
|
|
3254
|
+
justify-self: end;
|
|
3255
|
+
min-width: 0;
|
|
3256
|
+
color: var(--fg-muted);
|
|
3257
|
+
font-size: 12px;
|
|
3258
|
+
white-space: nowrap;
|
|
3259
|
+
}
|
|
3176
3260
|
.gdp-repo-empty {
|
|
3177
3261
|
padding: 32px;
|
|
3178
3262
|
color: var(--fg-muted);
|