|
|
@@ -0,0 +1,913 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="en">
|
|
|
+<head>
|
|
|
+<meta charset="UTF-8">
|
|
|
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+<title>GH-Hub</title>
|
|
|
+<style>
|
|
|
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
+:root {
|
|
|
+ --bg: var(--vscode-editor-background, #1e1e1e);
|
|
|
+ --bg2: var(--vscode-sideBar-background, #252526);
|
|
|
+ --bg3: var(--vscode-input-background, #3c3c3c);
|
|
|
+ --border: var(--vscode-panel-border, #474747);
|
|
|
+ --text: var(--vscode-editor-foreground, #cccccc);
|
|
|
+ --text2: var(--vscode-descriptionForeground, #858585);
|
|
|
+ --accent: var(--vscode-textLink-foreground, #3794ff);
|
|
|
+ --green: #3fb950; --yellow: #d29922; --red: #f85149; --purple: #a371f7;
|
|
|
+ --font: var(--vscode-font-family, system-ui);
|
|
|
+ --mono: var(--vscode-editor-font-family, monospace);
|
|
|
+ --radius: 4px;
|
|
|
+}
|
|
|
+body { font-family: var(--font); background: var(--bg); color: var(--text); font-size: 13px; line-height: 1.5; height: 100vh; display: flex; flex-direction: column; }
|
|
|
+a { color: var(--accent); text-decoration: none; }
|
|
|
+a:hover { text-decoration: underline; }
|
|
|
+
|
|
|
+button {
|
|
|
+ font-family: var(--font); font-size: 12px; padding: 4px 12px;
|
|
|
+ border: 1px solid var(--border); border-radius: var(--radius);
|
|
|
+ background: var(--bg3); color: var(--text); cursor: pointer;
|
|
|
+}
|
|
|
+button:hover { opacity: 0.85; }
|
|
|
+button.primary { background: #238636; border-color: #2ea043; color: #fff; }
|
|
|
+button.danger { background: #da3633; border-color: #f85149; color: #fff; }
|
|
|
+button.ai { background: #1f1a3e; border-color: var(--purple); color: var(--purple); }
|
|
|
+button:disabled { opacity: 0.4; cursor: default; }
|
|
|
+
|
|
|
+input, textarea, select {
|
|
|
+ font-family: var(--font); font-size: 12px; padding: 4px 8px;
|
|
|
+ border: 1px solid var(--border); border-radius: var(--radius);
|
|
|
+ background: var(--bg3); color: var(--text); width: 100%;
|
|
|
+}
|
|
|
+textarea { resize: vertical; min-height: 80px; font-family: var(--mono); font-size: 12px; }
|
|
|
+
|
|
|
+/* ─── Header ──────────────────────────────── */
|
|
|
+.header {
|
|
|
+ display: flex; align-items: center; gap: 10px; padding: 8px 14px;
|
|
|
+ background: var(--bg2); border-bottom: 1px solid var(--border);
|
|
|
+}
|
|
|
+.header h1 { font-size: 15px; font-weight: 600; }
|
|
|
+.header h1 b { color: var(--accent); }
|
|
|
+.header .user { color: var(--text2); font-size: 12px; }
|
|
|
+.header .spacer { flex: 1; }
|
|
|
+.badge {
|
|
|
+ background: var(--accent); color: #000; font-size: 10px; font-weight: 700;
|
|
|
+ padding: 1px 7px; border-radius: 8px; cursor: pointer; min-width: 18px; text-align: center;
|
|
|
+}
|
|
|
+.badge.zero { background: var(--bg3); color: var(--text2); }
|
|
|
+.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
|
|
|
+.dot.ok { background: var(--green); }
|
|
|
+.dot.err { background: var(--red); }
|
|
|
+
|
|
|
+/* ─── Tabs ────────────────────────────────── */
|
|
|
+.tabs {
|
|
|
+ display: flex; background: var(--bg2); border-bottom: 1px solid var(--border); padding: 0 14px;
|
|
|
+}
|
|
|
+.tab {
|
|
|
+ padding: 7px 14px; font-size: 12px; color: var(--text2); cursor: pointer;
|
|
|
+ border-bottom: 2px solid transparent;
|
|
|
+}
|
|
|
+.tab:hover { color: var(--text); }
|
|
|
+.tab.active { color: var(--text); border-bottom-color: var(--accent); }
|
|
|
+
|
|
|
+/* ─── Panels ──────────────────────────────── */
|
|
|
+.content { flex: 1; overflow: hidden; }
|
|
|
+.panel { display: none; height: 100%; overflow-y: auto; padding: 14px; }
|
|
|
+.panel.active { display: flex; flex-direction: column; }
|
|
|
+
|
|
|
+/* ─── Repos ───────────────────────────────── */
|
|
|
+.toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; }
|
|
|
+.toolbar h2 { font-size: 14px; flex: 1; }
|
|
|
+.repo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 8px; flex: 1; overflow-y: auto; }
|
|
|
+.repo-card {
|
|
|
+ background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
|
|
|
+ padding: 12px; cursor: pointer;
|
|
|
+}
|
|
|
+.repo-card:hover { border-color: var(--accent); }
|
|
|
+.repo-card.sel { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
|
|
+.repo-card h3 { font-size: 13px; color: var(--accent); }
|
|
|
+.repo-card .desc { color: var(--text2); font-size: 11px; margin: 2px 0 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
+.repo-meta { display: flex; gap: 12px; font-size: 11px; color: var(--text2); }
|
|
|
+.lang-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 2px; vertical-align: middle; }
|
|
|
+.tag-private { color: var(--yellow); border: 1px solid var(--yellow); padding: 0 4px; border-radius: 3px; font-size: 9px; }
|
|
|
+
|
|
|
+/* ─── Split View ──────────────────────────── */
|
|
|
+.split { display: flex; height: 100%; gap: 0; flex: 1; overflow: hidden; }
|
|
|
+.list-pane { width: 320px; min-width: 240px; border-right: 1px solid var(--border); overflow-y: auto; display: flex; flex-direction: column; }
|
|
|
+.detail-pane { flex: 1; overflow-y: auto; padding: 14px; }
|
|
|
+
|
|
|
+.filter-bar { padding: 6px 10px; border-bottom: 1px solid var(--border); display: flex; gap: 6px; align-items: center; }
|
|
|
+.filter-bar .label { font-size: 11px; color: var(--accent); font-weight: 600; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
+.filter-bar select { width: auto; font-size: 11px; }
|
|
|
+
|
|
|
+.item-card { padding: 8px 12px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
|
|
+.item-card:hover { background: var(--bg3); }
|
|
|
+.item-card.sel { background: var(--bg3); border-left: 3px solid var(--accent); }
|
|
|
+.item-card .num { color: var(--text2); font-size: 11px; }
|
|
|
+.item-card .title { font-size: 12px; font-weight: 500; }
|
|
|
+.item-card .meta { font-size: 10px; color: var(--text2); display: flex; gap: 8px; margin-top: 2px; }
|
|
|
+.label-tag { display: inline-block; font-size: 9px; font-weight: 600; padding: 0 5px; border-radius: 8px; margin-right: 3px; }
|
|
|
+
|
|
|
+/* ─── Detail ──────────────────────────────── */
|
|
|
+.detail-header h2 { font-size: 15px; margin-bottom: 2px; }
|
|
|
+.detail-header .meta { font-size: 11px; color: var(--text2); margin-bottom: 10px; }
|
|
|
+.detail-body {
|
|
|
+ background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
|
|
|
+ padding: 12px; margin-bottom: 12px; font-size: 12px; line-height: 1.6; word-wrap: break-word;
|
|
|
+}
|
|
|
+.detail-body pre { background: var(--bg); padding: 8px; border-radius: 3px; overflow-x: auto; font-family: var(--mono); font-size: 11px; margin: 6px 0; }
|
|
|
+.detail-body code { font-family: var(--mono); font-size: 11px; background: var(--bg3); padding: 1px 3px; border-radius: 2px; }
|
|
|
+.detail-body pre code { background: none; padding: 0; }
|
|
|
+
|
|
|
+.comment { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; margin-bottom: 6px; }
|
|
|
+.comment .author { font-weight: 600; font-size: 12px; }
|
|
|
+.comment .time { color: var(--text2); font-size: 10px; margin-left: 6px; }
|
|
|
+.comment .body { margin-top: 6px; font-size: 12px; line-height: 1.5; }
|
|
|
+
|
|
|
+.reply-area { margin-top: 12px; }
|
|
|
+.reply-actions { display: flex; gap: 6px; margin-top: 6px; align-items: center; }
|
|
|
+.confidence { font-size: 10px; padding: 1px 6px; border-radius: 8px; font-weight: 600; }
|
|
|
+.confidence.high { background: #0d3321; color: var(--green); }
|
|
|
+.confidence.medium { background: #3d2e00; color: var(--yellow); }
|
|
|
+.confidence.low { background: #3d1214; color: var(--red); }
|
|
|
+
|
|
|
+.empty { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text2); flex-direction: column; gap: 6px; padding: 30px; text-align: center; }
|
|
|
+
|
|
|
+/* ─── Notifications ───────────────────────── */
|
|
|
+.notif-item {
|
|
|
+ display: flex; gap: 10px; padding: 8px 12px; background: var(--bg2);
|
|
|
+ border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 6px; align-items: flex-start;
|
|
|
+}
|
|
|
+.notif-item .icon { font-size: 14px; margin-top: 2px; }
|
|
|
+.notif-item .info { flex: 1; }
|
|
|
+.notif-item .repo { font-size: 10px; color: var(--text2); }
|
|
|
+.notif-item .title { font-size: 12px; }
|
|
|
+.notif-item .reason { font-size: 10px; color: var(--text2); }
|
|
|
+
|
|
|
+/* ─── Push ────────────────────────────────── */
|
|
|
+.push-panel { max-width: 600px; }
|
|
|
+.field { margin-bottom: 10px; }
|
|
|
+.field label { font-size: 11px; color: var(--text2); margin-bottom: 3px; display: block; }
|
|
|
+.field-row { display: flex; gap: 6px; }
|
|
|
+.git-info { margin: 10px 0; }
|
|
|
+.git-info .branch { font-size: 13px; font-weight: 600; color: var(--green); }
|
|
|
+.git-info .counts { font-size: 11px; color: var(--text2); margin-top: 2px; }
|
|
|
+.git-info pre { background: var(--bg2); padding: 8px; border-radius: var(--radius); font-family: var(--mono); font-size: 11px; margin-top: 6px; max-height: 150px; overflow-y: auto; }
|
|
|
+.log { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px; font-family: var(--mono); font-size: 11px; min-height: 60px; white-space: pre-wrap; color: var(--text2); margin-top: 10px; }
|
|
|
+.log .ok { color: var(--green); }
|
|
|
+.log .err { color: var(--red); }
|
|
|
+
|
|
|
+/* ─── Publish Tab ─────────────────────────── */
|
|
|
+.pub-top { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-shrink: 0; }
|
|
|
+.pub-top h2 { font-size: 14px; flex: 1; }
|
|
|
+.pub-settings { display: flex; gap: 6px; align-items: center; flex-shrink: 0; margin-bottom: 8px; }
|
|
|
+.pub-settings select, .pub-settings input { width: auto; font-size: 11px; }
|
|
|
+.pub-settings input { max-width: 180px; }
|
|
|
+.pub-split { display: flex; flex: 1; gap: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius); }
|
|
|
+.pub-editor { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); }
|
|
|
+.pub-toolbar { display: flex; gap: 2px; padding: 4px 6px; background: var(--bg2); border-bottom: 1px solid var(--border); flex-wrap: wrap; }
|
|
|
+.pub-toolbar button { font-size: 11px; padding: 2px 7px; min-width: 26px; border: none; background: transparent; color: var(--text2); }
|
|
|
+.pub-toolbar button:hover { background: var(--bg3); color: var(--text); border-radius: 3px; }
|
|
|
+.pub-toolbar .sep { width: 1px; background: var(--border); margin: 2px 4px; }
|
|
|
+.pub-editor textarea { flex: 1; border: none; border-radius: 0; resize: none; padding: 10px; font-family: var(--mono); font-size: 12px; line-height: 1.6; background: var(--bg); }
|
|
|
+.pub-preview { flex: 1; overflow-y: auto; padding: 16px; background: var(--bg); font-size: 13px; line-height: 1.7; }
|
|
|
+.pub-preview h1 { font-size: 22px; border-bottom: 1px solid var(--border); padding-bottom: 6px; margin: 16px 0 10px; }
|
|
|
+.pub-preview h2 { font-size: 18px; border-bottom: 1px solid var(--border); padding-bottom: 4px; margin: 14px 0 8px; }
|
|
|
+.pub-preview h3 { font-size: 15px; margin: 12px 0 6px; }
|
|
|
+.pub-preview h4 { font-size: 13px; margin: 10px 0 4px; }
|
|
|
+.pub-preview p { margin: 6px 0; }
|
|
|
+.pub-preview img { max-width: 100%; border-radius: 4px; margin: 8px 0; }
|
|
|
+.pub-preview pre { background: var(--bg2); padding: 10px; border-radius: 4px; overflow-x: auto; font-family: var(--mono); font-size: 11px; margin: 8px 0; }
|
|
|
+.pub-preview code { font-family: var(--mono); font-size: 11px; background: var(--bg3); padding: 1px 4px; border-radius: 2px; }
|
|
|
+.pub-preview pre code { background: none; padding: 0; }
|
|
|
+.pub-preview blockquote { border-left: 3px solid var(--accent); padding: 4px 12px; margin: 8px 0; color: var(--text2); }
|
|
|
+.pub-preview table { border-collapse: collapse; margin: 8px 0; width: 100%; }
|
|
|
+.pub-preview th, .pub-preview td { border: 1px solid var(--border); padding: 4px 8px; font-size: 12px; text-align: left; }
|
|
|
+.pub-preview th { background: var(--bg2); }
|
|
|
+.pub-preview ul, .pub-preview ol { padding-left: 20px; margin: 6px 0; }
|
|
|
+.pub-preview li { margin: 2px 0; }
|
|
|
+.pub-preview hr { border: none; border-top: 1px solid var(--border); margin: 12px 0; }
|
|
|
+.pub-preview a { color: var(--accent); }
|
|
|
+.img-upload-area { padding: 6px 10px; background: var(--bg2); border-top: 1px solid var(--border); font-size: 11px; color: var(--text2); display: flex; align-items: center; gap: 6px; }
|
|
|
+.img-upload-area button { font-size: 11px; padding: 2px 8px; }
|
|
|
+
|
|
|
+/* ─── Modal ───────────────────────────────── */
|
|
|
+.modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 99; }
|
|
|
+.modal-bg.show { display: flex; }
|
|
|
+.modal { background: var(--bg2); border: 1px solid var(--border); border-radius: 6px; padding: 18px; width: 380px; }
|
|
|
+.modal h3 { margin-bottom: 12px; font-size: 14px; }
|
|
|
+.modal .actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 14px; }
|
|
|
+
|
|
|
+.spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .5s linear infinite; }
|
|
|
+@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
+.loading { display: flex; align-items: center; gap: 6px; padding: 20px; color: var(--text2); }
|
|
|
+</style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+
|
|
|
+<div class="header">
|
|
|
+ <h1>GH-<b>Hub</b></h1>
|
|
|
+ <span class="dot" id="hDot"></span>
|
|
|
+ <span class="user" id="hUser">...</span>
|
|
|
+ <span class="spacer"></span>
|
|
|
+ <span class="badge zero" id="hBadge" onclick="switchTab('notifications')">0</span>
|
|
|
+</div>
|
|
|
+
|
|
|
+<div class="tabs">
|
|
|
+ <div class="tab active" data-t="repos" onclick="switchTab('repos')">Repos</div>
|
|
|
+ <div class="tab" data-t="issues" onclick="switchTab('issues')">Issues</div>
|
|
|
+ <div class="tab" data-t="pulls" onclick="switchTab('pulls')">PRs</div>
|
|
|
+ <div class="tab" data-t="notifications" onclick="switchTab('notifications')">Notifications</div>
|
|
|
+ <div class="tab" data-t="push" onclick="switchTab('push')">Push</div>
|
|
|
+ <div class="tab" data-t="publish" onclick="switchTab('publish')">Publish</div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<div class="content">
|
|
|
+ <!-- ═══ Repos ═══ -->
|
|
|
+ <div class="panel active" id="p-repos">
|
|
|
+ <div class="toolbar">
|
|
|
+ <h2>Repositories</h2>
|
|
|
+ <button onclick="loadRepos()">Refresh</button>
|
|
|
+ <button class="primary" onclick="showModal('createRepoModal')">+ New</button>
|
|
|
+ </div>
|
|
|
+ <div class="repo-grid" id="repoGrid"><div class="loading"><span class="spinner"></span> Loading...</div></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ═══ Issues ═══ -->
|
|
|
+ <div class="panel" id="p-issues" style="padding:0;">
|
|
|
+ <div class="split">
|
|
|
+ <div class="list-pane">
|
|
|
+ <div class="filter-bar">
|
|
|
+ <span class="label" id="iRepoLabel">Select a repo</span>
|
|
|
+ <select id="iState" onchange="loadIssues()"><option value="open">Open</option><option value="closed">Closed</option><option value="all">All</option></select>
|
|
|
+ </div>
|
|
|
+ <div id="iList" style="flex:1;overflow-y:auto;"><div class="empty">Click a repo first</div></div>
|
|
|
+ </div>
|
|
|
+ <div class="detail-pane" id="iDetail"><div class="empty">Select an issue</div></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ═══ PRs ═══ -->
|
|
|
+ <div class="panel" id="p-pulls" style="padding:0;">
|
|
|
+ <div class="split">
|
|
|
+ <div class="list-pane">
|
|
|
+ <div class="filter-bar">
|
|
|
+ <span class="label" id="pRepoLabel">Select a repo</span>
|
|
|
+ <select id="pState" onchange="loadPRs()"><option value="open">Open</option><option value="closed">Closed</option><option value="all">All</option></select>
|
|
|
+ </div>
|
|
|
+ <div id="pList" style="flex:1;overflow-y:auto;"><div class="empty">Click a repo first</div></div>
|
|
|
+ </div>
|
|
|
+ <div class="detail-pane" id="pDetail"><div class="empty">Select a PR</div></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ═══ Notifications ═══ -->
|
|
|
+ <div class="panel" id="p-notifications">
|
|
|
+ <div class="toolbar">
|
|
|
+ <h2>Notifications</h2>
|
|
|
+ <button onclick="loadNotifs()">Refresh</button>
|
|
|
+ <button onclick="markAllRead()">Mark All Read</button>
|
|
|
+ </div>
|
|
|
+ <div id="notifList" style="flex:1;overflow-y:auto;max-width:650px;"><div class="loading"><span class="spinner"></span> Loading...</div></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ═══ Push ═══ -->
|
|
|
+ <div class="panel" id="p-push">
|
|
|
+ <h2 style="font-size:14px;margin-bottom:10px;">Quick Push</h2>
|
|
|
+ <div class="push-panel">
|
|
|
+ <div class="field">
|
|
|
+ <label>Local Directory</label>
|
|
|
+ <div class="field-row">
|
|
|
+ <input id="pushDir" placeholder="/path/to/project">
|
|
|
+ <button onclick="pickFolder()" style="white-space:nowrap;">Browse</button>
|
|
|
+ <button onclick="useWorkspace()" style="white-space:nowrap;">Workspace</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <button onclick="checkGit()">Check Status</button>
|
|
|
+ <div class="git-info" id="gitInfo"></div>
|
|
|
+ <div style="display:flex;gap:6px;margin-top:8px;">
|
|
|
+ <button class="primary" id="pushBtn" onclick="doPush()" disabled>Push</button>
|
|
|
+ <button onclick="doQuickSetup()">Quick Setup + Push</button>
|
|
|
+ </div>
|
|
|
+ <div class="log" id="pushLog" style="display:none;"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ═══ Publish ═══ -->
|
|
|
+ <div class="panel" id="p-publish">
|
|
|
+ <div class="pub-top">
|
|
|
+ <h2>Publish to GitHub</h2>
|
|
|
+ <span id="pubResult" style="font-size:12px;"></span>
|
|
|
+ </div>
|
|
|
+ <div class="pub-settings">
|
|
|
+ <select id="pubRepo" onchange="onRepoChange()"><option value="">Target Repo</option></select>
|
|
|
+ <input id="pubPath" value="README.md" style="max-width:140px;" placeholder="Path (e.g. README.md)">
|
|
|
+ <input id="pubMsg" value="Update README" style="max-width:200px;" placeholder="Commit message">
|
|
|
+ <button onclick="fetchExistingReadme()">Load Existing</button>
|
|
|
+ <button class="primary" onclick="doPublish()" id="pubBtn">Publish</button>
|
|
|
+ </div>
|
|
|
+ <div class="pub-split">
|
|
|
+ <div class="pub-editor">
|
|
|
+ <div class="pub-toolbar">
|
|
|
+ <button onclick="tbWrap('**','**')" title="Bold"><b>B</b></button>
|
|
|
+ <button onclick="tbWrap('*','*')" title="Italic"><i>I</i></button>
|
|
|
+ <button onclick="tbWrap('~~','~~')" title="Strikethrough"><s>S</s></button>
|
|
|
+ <div class="sep"></div>
|
|
|
+ <button onclick="tbPrefix('# ')" title="H1">H1</button>
|
|
|
+ <button onclick="tbPrefix('## ')" title="H2">H2</button>
|
|
|
+ <button onclick="tbPrefix('### ')" title="H3">H3</button>
|
|
|
+ <div class="sep"></div>
|
|
|
+ <button onclick="tbPrefix('- ')" title="Bullet list">•</button>
|
|
|
+ <button onclick="tbPrefix('1. ')" title="Numbered list">1.</button>
|
|
|
+ <button onclick="tbPrefix('> ')" title="Blockquote">></button>
|
|
|
+ <div class="sep"></div>
|
|
|
+ <button onclick="tbLink()" title="Insert link">Link</button>
|
|
|
+ <button onclick="tbImage()" title="Insert image from URL">Img</button>
|
|
|
+ <button onclick="tbUploadImage()" title="Upload image to repo">Upload</button>
|
|
|
+ <div class="sep"></div>
|
|
|
+ <button onclick="tbWrap('`','`')" title="Inline code">code</button>
|
|
|
+ <button onclick="tbCodeBlock()" title="Code block">```</button>
|
|
|
+ <button onclick="tbPrefix('---')" title="Horizontal rule">HR</button>
|
|
|
+ <div class="sep"></div>
|
|
|
+ <button onclick="tbTable()" title="Insert table">Table</button>
|
|
|
+ </div>
|
|
|
+ <textarea id="pubEditor" placeholder="Write your markdown here..." oninput="updatePreview()"></textarea>
|
|
|
+ <div class="img-upload-area">
|
|
|
+ <span>Images:</span>
|
|
|
+ <button onclick="tbUploadImage()">Browse & Upload</button>
|
|
|
+ <span id="imgStatus" style="color:var(--text2);font-size:10px;"></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="pub-preview" id="pubPreview">
|
|
|
+ <div class="empty" style="height:auto;">Live preview will appear here</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<!-- ═══ Create Repo Modal ═══ -->
|
|
|
+<div class="modal-bg" id="createRepoModal">
|
|
|
+ <div class="modal">
|
|
|
+ <h3>New Repository</h3>
|
|
|
+ <div class="field"><label>Name</label><input id="nrName" placeholder="my-project"></div>
|
|
|
+ <div class="field"><label>Description</label><input id="nrDesc" placeholder="optional"></div>
|
|
|
+ <div class="field"><label><input type="checkbox" id="nrPrivate" style="width:auto;margin-right:4px;">Private</label></div>
|
|
|
+ <div class="actions">
|
|
|
+ <button onclick="hideModal('createRepoModal')">Cancel</button>
|
|
|
+ <button class="primary" onclick="createRepo()">Create</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<!-- ═══ Quick Setup Modal ═══ -->
|
|
|
+<div class="modal-bg" id="quickSetupModal">
|
|
|
+ <div class="modal">
|
|
|
+ <h3>Quick Setup + Push</h3>
|
|
|
+ <p style="font-size:11px;color:var(--text2);margin-bottom:10px;">Create repo, init git, commit all, and push in one step.</p>
|
|
|
+ <div class="field"><label>Repo Name (owner/name)</label><input id="qsName" placeholder="GeneralNoCodeDP/my-project"></div>
|
|
|
+ <div class="field"><label>Description</label><input id="qsDesc"></div>
|
|
|
+ <div class="field"><label>Commit Message</label><input id="qsMsg" value="Initial commit"></div>
|
|
|
+ <div class="field"><label><input type="checkbox" id="qsPrivate" style="width:auto;margin-right:4px;">Private</label></div>
|
|
|
+ <div class="actions">
|
|
|
+ <button onclick="hideModal('quickSetupModal')">Cancel</button>
|
|
|
+ <button class="primary" onclick="execQuickSetup()">Go</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<script>
|
|
|
+const vscode = acquireVsCodeApi();
|
|
|
+let S = { repo: null, repos: [], aiReady: false, pubContent: '', pendingCallbacks: {} };
|
|
|
+let msgId = 0;
|
|
|
+
|
|
|
+// ─── Bridge ──────────────────────────────────
|
|
|
+
|
|
|
+function send(command, args = {}) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const id = ++msgId;
|
|
|
+ S.pendingCallbacks[id] = { resolve, reject };
|
|
|
+ vscode.postMessage({ id, command, args });
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+window.addEventListener('message', e => {
|
|
|
+ const msg = e.data;
|
|
|
+ // Async response
|
|
|
+ if (msg.id && S.pendingCallbacks[msg.id]) {
|
|
|
+ const cb = S.pendingCallbacks[msg.id];
|
|
|
+ delete S.pendingCallbacks[msg.id];
|
|
|
+ if (msg.error) cb.reject(new Error(msg.error));
|
|
|
+ else cb.resolve(msg.data);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // Push events
|
|
|
+ if (msg.command === 'notifUpdate') {
|
|
|
+ const n = Array.isArray(msg.data) ? msg.data.length : 0;
|
|
|
+ setBadge(n);
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// ─── Tabs ────────────────────────────────────
|
|
|
+
|
|
|
+function switchTab(t) {
|
|
|
+ document.querySelectorAll('.tab').forEach(el => el.classList.toggle('active', el.dataset.t === t));
|
|
|
+ document.querySelectorAll('.panel').forEach(el => el.classList.toggle('active', el.id === `p-${t}`));
|
|
|
+ if (t === 'notifications') loadNotifs();
|
|
|
+ if (t === 'publish') populateRepoSelect();
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Init ────────────────────────────────────
|
|
|
+
|
|
|
+async function init() {
|
|
|
+ try {
|
|
|
+ const s = await send('status');
|
|
|
+ document.getElementById('hDot').className = s.ok ? 'dot ok' : 'dot err';
|
|
|
+ document.getElementById('hUser').textContent = s.ok ? s.user : 'Not authenticated';
|
|
|
+ S.aiReady = s.aiReady;
|
|
|
+ } catch (e) {
|
|
|
+ document.getElementById('hDot').className = 'dot err';
|
|
|
+ document.getElementById('hUser').textContent = 'Error';
|
|
|
+ }
|
|
|
+ loadRepos();
|
|
|
+}
|
|
|
+
|
|
|
+function setBadge(n) {
|
|
|
+ const b = document.getElementById('hBadge');
|
|
|
+ b.textContent = n;
|
|
|
+ b.className = n > 0 ? 'badge' : 'badge zero';
|
|
|
+}
|
|
|
+
|
|
|
+function showModal(id) { document.getElementById(id).classList.add('show'); }
|
|
|
+function hideModal(id) { document.getElementById(id).classList.remove('show'); }
|
|
|
+
|
|
|
+// ─── Repos ───────────────────────────────────
|
|
|
+
|
|
|
+const LC = { JavaScript:'#f1e05a', TypeScript:'#3178c6', Python:'#3572a5', Go:'#00add8', Rust:'#dea584', HTML:'#e34c26', CSS:'#563d7c', Shell:'#89e051', Vue:'#41b883' };
|
|
|
+
|
|
|
+async function loadRepos() {
|
|
|
+ const g = document.getElementById('repoGrid');
|
|
|
+ g.innerHTML = '<div class="loading"><span class="spinner"></span> Loading...</div>';
|
|
|
+ try {
|
|
|
+ const repos = await send('listRepos');
|
|
|
+ S.repos = repos || [];
|
|
|
+ if (!repos?.length) { g.innerHTML = '<div class="empty">No repos</div>'; return; }
|
|
|
+ g.innerHTML = repos.map(r => {
|
|
|
+ const nwo = r.owner.login + '/' + r.name;
|
|
|
+ const l = r.primaryLanguage?.name || '';
|
|
|
+ const c = LC[l] || '#858585';
|
|
|
+ return `<div class="repo-card${S.repo===nwo?' sel':''}" onclick="selectRepo('${nwo}')" data-r="${nwo}">
|
|
|
+ <h3>${r.name}</h3>
|
|
|
+ <div class="desc">${esc(r.description||'No description')}</div>
|
|
|
+ <div class="repo-meta">
|
|
|
+ ${l?`<span><span class="lang-dot" style="background:${c}"></span>${l}</span>`:''}
|
|
|
+ <span>★${r.stargazerCount}</span>
|
|
|
+ <span>${timeAgo(r.updatedAt)}</span>
|
|
|
+ ${r.isPrivate?'<span class="tag-private">Private</span>':''}
|
|
|
+ </div></div>`;
|
|
|
+ }).join('');
|
|
|
+ } catch (e) { g.innerHTML = `<div class="empty">Error: ${esc(e.message)}</div>`; }
|
|
|
+}
|
|
|
+
|
|
|
+function selectRepo(nwo) {
|
|
|
+ S.repo = nwo;
|
|
|
+ document.querySelectorAll('.repo-card').forEach(c => c.classList.toggle('sel', c.dataset.r === nwo));
|
|
|
+ document.getElementById('iRepoLabel').textContent = nwo;
|
|
|
+ document.getElementById('pRepoLabel').textContent = nwo;
|
|
|
+ loadIssues(); loadPRs();
|
|
|
+}
|
|
|
+
|
|
|
+async function createRepo() {
|
|
|
+ const name = document.getElementById('nrName').value.trim();
|
|
|
+ if (!name) return;
|
|
|
+ try {
|
|
|
+ await send('createRepo', { name, description: document.getElementById('nrDesc').value.trim(), isPrivate: document.getElementById('nrPrivate').checked });
|
|
|
+ hideModal('createRepoModal');
|
|
|
+ document.getElementById('nrName').value = '';
|
|
|
+ document.getElementById('nrDesc').value = '';
|
|
|
+ loadRepos();
|
|
|
+ } catch (e) { alert('Error: ' + e.message); }
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Issues ──────────────────────────────────
|
|
|
+
|
|
|
+async function loadIssues() {
|
|
|
+ if (!S.repo) return;
|
|
|
+ const el = document.getElementById('iList');
|
|
|
+ el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
|
|
|
+ try {
|
|
|
+ const items = await send('listIssues', { repo: S.repo, state: document.getElementById('iState').value });
|
|
|
+ if (!items?.length) { el.innerHTML = '<div class="empty">No issues</div>'; return; }
|
|
|
+ el.innerHTML = items.map(i => `<div class="item-card" onclick="viewIssue(${i.number})">
|
|
|
+ <div class="num">#${i.number}</div>
|
|
|
+ <div class="title">${esc(i.title)}</div>
|
|
|
+ <div class="meta"><span>${i.author?.login||'?'}</span><span>${timeAgo(i.createdAt)}</span><span>${i.comments||0} comments</span></div>
|
|
|
+ ${(i.labels||[]).map(l=>`<span class="label-tag" style="background:${l.color?'#'+l.color+'33':'var(--bg3)'};color:${l.color?'#'+l.color:'var(--text2)'}">${esc(l.name)}</span>`).join('')}
|
|
|
+ </div>`).join('');
|
|
|
+ } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
|
|
|
+}
|
|
|
+
|
|
|
+async function viewIssue(num) {
|
|
|
+ const el = document.getElementById('iDetail');
|
|
|
+ el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
|
|
|
+ document.querySelectorAll('#iList .item-card').forEach(c => c.classList.toggle('sel', c.querySelector('.num')?.textContent === '#'+num));
|
|
|
+ try {
|
|
|
+ const d = await send('viewIssue', { repo: S.repo, number: num });
|
|
|
+ const cm = d.commentList || [];
|
|
|
+ el.innerHTML = `
|
|
|
+ <div class="detail-header"><h2>${esc(d.title)} <span style="color:var(--text2);font-weight:400">#${d.number}</span></h2>
|
|
|
+ <div class="meta"><span style="color:${d.state==='OPEN'?'var(--green)':'var(--purple)'}">${d.state}</span> · ${d.author?.login||'?'} · ${timeAgo(d.createdAt)}</div></div>
|
|
|
+ <div class="detail-body">${renderMd(d.body||'(empty)')}</div>
|
|
|
+ ${cm.length?cm.map(c=>`<div class="comment"><span class="author">${esc(c.author)}</span><span class="time">${timeAgo(c.createdAt)}</span><div class="body">${renderMd(c.body)}</div></div>`).join(''):''}
|
|
|
+ <div class="reply-area"><textarea id="iReply" placeholder="Write a reply..."></textarea>
|
|
|
+ <div class="reply-actions">
|
|
|
+ ${S.aiReady?`<button class="ai" onclick="aiDraft('issue',${num})">AI Draft</button>`:''}<span id="iConf"></span><span style="flex:1"></span>
|
|
|
+ <button class="primary" onclick="postIssueComment(${num})">Reply</button>
|
|
|
+ ${d.state==='OPEN'?`<button class="danger" onclick="doCloseIssue(${num})">Close</button>`:`<button onclick="doReopenIssue(${num})">Reopen</button>`}
|
|
|
+ </div></div>`;
|
|
|
+ } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
|
|
|
+}
|
|
|
+
|
|
|
+async function postIssueComment(num) {
|
|
|
+ const body = document.getElementById('iReply').value.trim();
|
|
|
+ if (!body) return;
|
|
|
+ await send('commentIssue', { repo: S.repo, number: num, body });
|
|
|
+ viewIssue(num);
|
|
|
+}
|
|
|
+async function doCloseIssue(num) { await send('closeIssue', { repo: S.repo, number: num }); viewIssue(num); loadIssues(); }
|
|
|
+async function doReopenIssue(num) { await send('reopenIssue', { repo: S.repo, number: num }); viewIssue(num); loadIssues(); }
|
|
|
+
|
|
|
+// ─── PRs ─────────────────────────────────────
|
|
|
+
|
|
|
+async function loadPRs() {
|
|
|
+ if (!S.repo) return;
|
|
|
+ const el = document.getElementById('pList');
|
|
|
+ el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
|
|
|
+ try {
|
|
|
+ const items = await send('listPRs', { repo: S.repo, state: document.getElementById('pState').value });
|
|
|
+ if (!items?.length) { el.innerHTML = '<div class="empty">No PRs</div>'; return; }
|
|
|
+ el.innerHTML = items.map(p => `<div class="item-card" onclick="viewPR(${p.number})">
|
|
|
+ <div class="num">#${p.number}${p.isDraft?' <span style="color:var(--text2)">(Draft)</span>':''}</div>
|
|
|
+ <div class="title">${esc(p.title)}</div>
|
|
|
+ <div class="meta"><span>${p.author?.login||'?'}</span><span>${p.headRefName} → ${p.baseRefName}</span><span>${timeAgo(p.createdAt)}</span></div>
|
|
|
+ </div>`).join('');
|
|
|
+ } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
|
|
|
+}
|
|
|
+
|
|
|
+async function viewPR(num) {
|
|
|
+ const el = document.getElementById('pDetail');
|
|
|
+ el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
|
|
|
+ document.querySelectorAll('#pList .item-card').forEach(c => c.classList.toggle('sel', c.querySelector('.num')?.textContent?.startsWith('#'+num)));
|
|
|
+ try {
|
|
|
+ const d = await send('viewPR', { repo: S.repo, number: num });
|
|
|
+ const cm = d.commentList || [];
|
|
|
+ const sc = d.state==='OPEN'?'var(--green)':d.state==='MERGED'?'var(--purple)':'var(--red)';
|
|
|
+ el.innerHTML = `
|
|
|
+ <div class="detail-header"><h2>${esc(d.title)} <span style="color:var(--text2);font-weight:400">#${d.number}</span></h2>
|
|
|
+ <div class="meta"><span style="color:${sc}">${d.state}</span> · ${d.author?.login||'?'} · ${d.headRefName}→${d.baseRefName} · +${d.additions||0} -${d.deletions||0} (${d.changedFiles||0} files)</div></div>
|
|
|
+ <div class="detail-body">${renderMd(d.body||'(empty)')}</div>
|
|
|
+ ${cm.length?cm.map(c=>`<div class="comment"><span class="author">${esc(c.author)}</span><span class="time">${timeAgo(c.createdAt)}</span><div class="body">${renderMd(c.body)}</div></div>`).join(''):''}
|
|
|
+ <div class="reply-area"><textarea id="pReply" placeholder="Write a comment..."></textarea>
|
|
|
+ <div class="reply-actions">
|
|
|
+ ${S.aiReady?`<button class="ai" onclick="aiDraft('pr',${num})">AI Draft</button>`:''}<span id="pConf"></span><span style="flex:1"></span>
|
|
|
+ <button class="primary" onclick="postPRComment(${num})">Comment</button>
|
|
|
+ ${d.state==='OPEN'?`<button class="primary" onclick="doMergePR(${num})">Merge</button><button class="danger" onclick="doClosePR(${num})">Close</button>`:''}
|
|
|
+ </div></div>`;
|
|
|
+ } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
|
|
|
+}
|
|
|
+
|
|
|
+async function postPRComment(num) { const b = document.getElementById('pReply').value.trim(); if (!b) return; await send('commentPR', { repo: S.repo, number: num, body: b }); viewPR(num); }
|
|
|
+async function doMergePR(num) { await send('mergePR', { repo: S.repo, number: num, method: 'merge' }); viewPR(num); loadPRs(); }
|
|
|
+async function doClosePR(num) { await send('closePR', { repo: S.repo, number: num }); viewPR(num); loadPRs(); }
|
|
|
+
|
|
|
+// ─── AI Draft ────────────────────────────────
|
|
|
+
|
|
|
+async function aiDraft(type, num) {
|
|
|
+ const tid = type==='issue'?'iReply':'pReply';
|
|
|
+ const cid = type==='issue'?'iConf':'pConf';
|
|
|
+ const ta = document.getElementById(tid);
|
|
|
+ const conf = document.getElementById(cid);
|
|
|
+ ta.value = 'Generating...'; ta.disabled = true;
|
|
|
+ conf.innerHTML = '<span class="spinner"></span>';
|
|
|
+ try {
|
|
|
+ const detail = await send(type==='issue'?'viewIssue':'viewPR', { repo: S.repo, number: num });
|
|
|
+ const res = await send('aiDraft', { type, title: detail.title, body: detail.body, comments: detail.commentList, repoName: S.repo });
|
|
|
+ ta.value = res.draft;
|
|
|
+ conf.innerHTML = `<span class="confidence ${res.confidence}">${res.confidence}</span>`;
|
|
|
+ } catch (e) { ta.value = ''; conf.textContent = ''; alert('AI error: '+e.message); }
|
|
|
+ ta.disabled = false; ta.focus();
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Notifications ───────────────────────────
|
|
|
+
|
|
|
+const NI = { Issue:'<span style="color:var(--green)">●</span>', PullRequest:'<span style="color:var(--purple)">⤮</span>', Release:'<span style="color:var(--accent)">♦</span>' };
|
|
|
+
|
|
|
+async function loadNotifs() {
|
|
|
+ const el = document.getElementById('notifList');
|
|
|
+ el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
|
|
|
+ try {
|
|
|
+ const items = await send('listNotifications');
|
|
|
+ setBadge(items?.length || 0);
|
|
|
+ if (!items?.length) { el.innerHTML = '<div class="empty">No unread notifications</div>'; return; }
|
|
|
+ el.innerHTML = items.map(n => `<div class="notif-item">
|
|
|
+ <div class="icon">${NI[n.type]||'●'}</div>
|
|
|
+ <div class="info"><div class="repo">${esc(n.repo)}</div><div class="title">${esc(n.title)}</div><div class="reason">${n.reason} · ${timeAgo(n.updatedAt)}</div></div>
|
|
|
+ <button onclick="markRead('${n.id}',this.parentElement)">Read</button>
|
|
|
+ </div>`).join('');
|
|
|
+ } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
|
|
|
+}
|
|
|
+
|
|
|
+async function markRead(id, el) { await send('markRead', { id }); if (el) el.style.opacity = '0.3'; }
|
|
|
+async function markAllRead() { await send('markAllRead'); loadNotifs(); }
|
|
|
+
|
|
|
+// ─── Push ────────────────────────────────────
|
|
|
+
|
|
|
+async function pickFolder() { const p = await send('pickFolder'); if (p) document.getElementById('pushDir').value = p; }
|
|
|
+async function useWorkspace() { const p = await send('getWorkspaceDir'); if (p) document.getElementById('pushDir').value = p; }
|
|
|
+
|
|
|
+async function checkGit() {
|
|
|
+ const dir = document.getElementById('pushDir').value.trim(); if (!dir) return;
|
|
|
+ const el = document.getElementById('gitInfo');
|
|
|
+ el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
|
|
|
+ try {
|
|
|
+ const s = await send('gitStatus', { dir });
|
|
|
+ el.innerHTML = `<div class="branch">${esc(s.branch)}</div>
|
|
|
+ <div class="counts">${s.ahead>0?`<span style="color:var(--green)">${s.ahead} ahead</span> `:''}${s.behind>0?`<span style="color:var(--red)">${s.behind} behind</span> `:''}${s.ahead===0&&s.behind===0?'Up to date':''}</div>
|
|
|
+ ${s.status?`<pre>${esc(s.status)}</pre>`:'<pre style="color:var(--text2)">Clean</pre>'}`;
|
|
|
+ document.getElementById('pushBtn').disabled = false;
|
|
|
+ } catch (e) { el.innerHTML = `<span style="color:var(--red)">${esc(e.message)}</span>`; document.getElementById('pushBtn').disabled = true; }
|
|
|
+}
|
|
|
+
|
|
|
+async function doPush() {
|
|
|
+ const dir = document.getElementById('pushDir').value.trim(); if (!dir) return;
|
|
|
+ const log = document.getElementById('pushLog');
|
|
|
+ log.style.display = 'block'; log.innerHTML = 'Pushing...\n';
|
|
|
+ document.getElementById('pushBtn').disabled = true;
|
|
|
+ try {
|
|
|
+ const r = await send('gitPush', { dir });
|
|
|
+ log.innerHTML += `<span class="ok">${esc(typeof r==='string'?r:r?.message||'Done')}</span>\n`;
|
|
|
+ } catch (e) { log.innerHTML += `<span class="err">Error: ${esc(e.message)}</span>\n`; }
|
|
|
+ document.getElementById('pushBtn').disabled = false;
|
|
|
+ checkGit();
|
|
|
+}
|
|
|
+
|
|
|
+function doQuickSetup() {
|
|
|
+ const dir = document.getElementById('pushDir').value.trim();
|
|
|
+ if (!dir) { alert('Set directory first'); return; }
|
|
|
+ const n = dir.split('/').pop();
|
|
|
+ document.getElementById('qsName').value = `GeneralNoCodeDP/${n}`;
|
|
|
+ showModal('quickSetupModal');
|
|
|
+}
|
|
|
+
|
|
|
+async function execQuickSetup() {
|
|
|
+ const dir = document.getElementById('pushDir').value.trim();
|
|
|
+ const log = document.getElementById('pushLog');
|
|
|
+ log.style.display = 'block'; log.innerHTML = 'Setting up...\n';
|
|
|
+ hideModal('quickSetupModal');
|
|
|
+ try {
|
|
|
+ const r = await send('quickSetup', {
|
|
|
+ dir,
|
|
|
+ createRepo: true,
|
|
|
+ repoName: document.getElementById('qsName').value.trim(),
|
|
|
+ description: document.getElementById('qsDesc').value.trim(),
|
|
|
+ isPrivate: document.getElementById('qsPrivate').checked,
|
|
|
+ commitMessage: document.getElementById('qsMsg').value.trim() || 'Initial commit'
|
|
|
+ });
|
|
|
+ log.innerHTML += `<span class="ok">${esc(r?.message||'Done!')}</span>\n`;
|
|
|
+ } catch (e) { log.innerHTML += `<span class="err">Error: ${esc(e.message)}</span>\n`; }
|
|
|
+ checkGit();
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Publish ─────────────────────────────────
|
|
|
+
|
|
|
+function populateRepoSelect() {
|
|
|
+ const sel = document.getElementById('pubRepo');
|
|
|
+ const cur = sel.value;
|
|
|
+ sel.innerHTML = '<option value="">Target Repo</option>' + S.repos.map(r => {
|
|
|
+ const nwo = r.owner.login+'/'+r.name;
|
|
|
+ return `<option value="${nwo}"${nwo===cur?' selected':''}>${nwo}</option>`;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function onRepoChange() {
|
|
|
+ // Auto-load existing README when repo changes
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchExistingReadme() {
|
|
|
+ const repo = document.getElementById('pubRepo').value;
|
|
|
+ if (!repo) { alert('Select a repo first'); return; }
|
|
|
+ const editor = document.getElementById('pubEditor');
|
|
|
+ const res = document.getElementById('pubResult');
|
|
|
+ res.innerHTML = '<span class="spinner"></span> Loading...';
|
|
|
+ try {
|
|
|
+ const data = await send('fetchReadme', { repo });
|
|
|
+ if (data.content) {
|
|
|
+ editor.value = data.content;
|
|
|
+ updatePreview();
|
|
|
+ res.innerHTML = '<span style="color:var(--green)">Loaded existing README</span>';
|
|
|
+ } else {
|
|
|
+ res.innerHTML = '<span style="color:var(--text2)">No existing README found</span>';
|
|
|
+ }
|
|
|
+ } catch (e) { res.innerHTML = `<span style="color:var(--red)">Error: ${esc(e.message)}</span>`; }
|
|
|
+}
|
|
|
+
|
|
|
+function updatePreview() {
|
|
|
+ const md = document.getElementById('pubEditor').value;
|
|
|
+ const el = document.getElementById('pubPreview');
|
|
|
+ if (!md.trim()) { el.innerHTML = '<div class="empty" style="height:auto;">Live preview will appear here</div>'; return; }
|
|
|
+ el.innerHTML = renderFullMd(md);
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Toolbar Actions ────────────────────────
|
|
|
+
|
|
|
+function getEditor() { return document.getElementById('pubEditor'); }
|
|
|
+
|
|
|
+function tbWrap(before, after) {
|
|
|
+ const ta = getEditor();
|
|
|
+ const start = ta.selectionStart, end = ta.selectionEnd;
|
|
|
+ const sel = ta.value.substring(start, end) || 'text';
|
|
|
+ ta.value = ta.value.substring(0, start) + before + sel + after + ta.value.substring(end);
|
|
|
+ ta.selectionStart = start + before.length;
|
|
|
+ ta.selectionEnd = start + before.length + sel.length;
|
|
|
+ ta.focus(); updatePreview();
|
|
|
+}
|
|
|
+
|
|
|
+function tbPrefix(prefix) {
|
|
|
+ const ta = getEditor();
|
|
|
+ const start = ta.selectionStart;
|
|
|
+ // Find line start
|
|
|
+ const lineStart = ta.value.lastIndexOf('\n', start - 1) + 1;
|
|
|
+ ta.value = ta.value.substring(0, lineStart) + prefix + ta.value.substring(lineStart);
|
|
|
+ ta.selectionStart = ta.selectionEnd = start + prefix.length;
|
|
|
+ ta.focus(); updatePreview();
|
|
|
+}
|
|
|
+
|
|
|
+function tbLink() {
|
|
|
+ const ta = getEditor();
|
|
|
+ const sel = ta.value.substring(ta.selectionStart, ta.selectionEnd) || 'link text';
|
|
|
+ const start = ta.selectionStart;
|
|
|
+ const ins = `[${sel}](url)`;
|
|
|
+ ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
|
|
|
+ // Select "url" for easy replacement
|
|
|
+ ta.selectionStart = start + sel.length + 3;
|
|
|
+ ta.selectionEnd = start + sel.length + 6;
|
|
|
+ ta.focus(); updatePreview();
|
|
|
+}
|
|
|
+
|
|
|
+function tbImage() {
|
|
|
+ const ta = getEditor();
|
|
|
+ const start = ta.selectionStart;
|
|
|
+ const ins = '';
|
|
|
+ ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
|
|
|
+ ta.selectionStart = start + 12;
|
|
|
+ ta.selectionEnd = start + 21;
|
|
|
+ ta.focus(); updatePreview();
|
|
|
+}
|
|
|
+
|
|
|
+function tbCodeBlock() {
|
|
|
+ const ta = getEditor();
|
|
|
+ const sel = ta.value.substring(ta.selectionStart, ta.selectionEnd) || 'code here';
|
|
|
+ const start = ta.selectionStart;
|
|
|
+ const ins = '```\n' + sel + '\n```';
|
|
|
+ ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
|
|
|
+ ta.selectionStart = start + 4;
|
|
|
+ ta.selectionEnd = start + 4 + sel.length;
|
|
|
+ ta.focus(); updatePreview();
|
|
|
+}
|
|
|
+
|
|
|
+function tbTable() {
|
|
|
+ const ta = getEditor();
|
|
|
+ const start = ta.selectionStart;
|
|
|
+ const ins = '\n| Column 1 | Column 2 | Column 3 |\n|----------|----------|----------|\n| Cell 1 | Cell 2 | Cell 3 |\n| Cell 4 | Cell 5 | Cell 6 |\n';
|
|
|
+ ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
|
|
|
+ ta.selectionStart = start + 3;
|
|
|
+ ta.selectionEnd = start + 11;
|
|
|
+ ta.focus(); updatePreview();
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Image Upload ───────────────────────────
|
|
|
+
|
|
|
+async function tbUploadImage() {
|
|
|
+ const repo = document.getElementById('pubRepo').value;
|
|
|
+ if (!repo) { alert('Select a target repo first — images will be uploaded there'); return; }
|
|
|
+ const status = document.getElementById('imgStatus');
|
|
|
+ status.textContent = 'Picking image...';
|
|
|
+ try {
|
|
|
+ const img = await send('pickImage');
|
|
|
+ if (!img) { status.textContent = ''; return; }
|
|
|
+ status.textContent = `Uploading ${img.name} (${(img.size/1024).toFixed(1)}KB)...`;
|
|
|
+ const filePath = `assets/images/${img.name}`;
|
|
|
+ const result = await send('uploadImage', { repo, filePath, base64: img.base64, message: `Upload ${img.name}` });
|
|
|
+ // Insert markdown image at cursor
|
|
|
+ const ta = getEditor();
|
|
|
+ const pos = ta.selectionStart;
|
|
|
+ const imgMd = `\n`;
|
|
|
+ ta.value = ta.value.substring(0, pos) + imgMd + ta.value.substring(ta.selectionEnd);
|
|
|
+ ta.selectionStart = ta.selectionEnd = pos + imgMd.length;
|
|
|
+ updatePreview();
|
|
|
+ status.textContent = `Uploaded ${img.name}`;
|
|
|
+ setTimeout(() => { status.textContent = ''; }, 3000);
|
|
|
+ } catch (e) { status.textContent = `Error: ${e.message}`; }
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Publish Action ─────────────────────────
|
|
|
+
|
|
|
+async function doPublish() {
|
|
|
+ const content = document.getElementById('pubEditor').value;
|
|
|
+ if (!content.trim()) { alert('Write some content first'); return; }
|
|
|
+ const repo = document.getElementById('pubRepo').value;
|
|
|
+ if (!repo) { alert('Select a repo'); return; }
|
|
|
+ const fpath = document.getElementById('pubPath').value.trim() || 'README.md';
|
|
|
+ const msg = document.getElementById('pubMsg').value.trim() || 'Update file';
|
|
|
+ const res = document.getElementById('pubResult');
|
|
|
+ res.innerHTML = '<span class="spinner"></span> Publishing...';
|
|
|
+ document.getElementById('pubBtn').disabled = true;
|
|
|
+ try {
|
|
|
+ await send('updateFile', { repo, content, path: fpath, message: msg });
|
|
|
+ res.innerHTML = `<span style="color:var(--green)">Published ${esc(fpath)} to ${esc(repo)}</span>`;
|
|
|
+ } catch (e) { res.innerHTML = `<span style="color:var(--red)">Error: ${esc(e.message)}</span>`; }
|
|
|
+ document.getElementById('pubBtn').disabled = false;
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Utils ───────────────────────────────────
|
|
|
+
|
|
|
+function esc(s) { return s==null?'':String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
|
+
|
|
|
+function timeAgo(d) {
|
|
|
+ if (!d) return '';
|
|
|
+ const m = Math.floor((Date.now()-new Date(d))/60000);
|
|
|
+ if (m<1) return 'just now'; if (m<60) return m+'m'; const h=Math.floor(m/60);
|
|
|
+ if (h<24) return h+'h'; const dy=Math.floor(h/24); if (dy<30) return dy+'d';
|
|
|
+ return new Date(d).toLocaleDateString();
|
|
|
+}
|
|
|
+
|
|
|
+function renderMd(t) {
|
|
|
+ if (!t) return '';
|
|
|
+ let h = esc(t);
|
|
|
+ h = h.replace(/```(\w*)\n([\s\S]*?)```/g,'<pre><code>$2</code></pre>');
|
|
|
+ h = h.replace(/`([^`]+)`/g,'<code>$1</code>');
|
|
|
+ h = h.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');
|
|
|
+ h = h.replace(/\*(.+?)\*/g,'<em>$1</em>');
|
|
|
+ h = h.replace(/^### (.+)$/gm,'<h4 style="margin:6px 0 3px">$1</h4>');
|
|
|
+ h = h.replace(/^## (.+)$/gm,'<h3 style="margin:8px 0 4px">$1</h3>');
|
|
|
+ h = h.replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" target="_blank">$1</a>');
|
|
|
+ h = h.replace(/^- (.+)$/gm,'• $1');
|
|
|
+ h = h.replace(/\n/g,'<br>');
|
|
|
+ return h;
|
|
|
+}
|
|
|
+
|
|
|
+function renderFullMd(t) {
|
|
|
+ if (!t) return '';
|
|
|
+ let h = esc(t);
|
|
|
+ // Code blocks (fenced)
|
|
|
+ h = h.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
|
|
|
+ // Inline code
|
|
|
+ h = h.replace(/`([^`\n]+)`/g, '<code>$1</code>');
|
|
|
+ // Images (before links)
|
|
|
+ h = h.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
|
|
|
+ // Links
|
|
|
+ h = h.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
|
+ // Bold + Italic
|
|
|
+ h = h.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
|
+ h = h.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
|
+ h = h.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
|
+ h = h.replace(/~~(.+?)~~/g, '<del>$1</del>');
|
|
|
+ // Headings
|
|
|
+ h = h.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
|
|
|
+ h = h.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
|
|
+ h = h.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
|
|
+ h = h.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
|
|
+ // Horizontal rule
|
|
|
+ h = h.replace(/^---+$/gm, '<hr>');
|
|
|
+ // Blockquote
|
|
|
+ h = h.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>');
|
|
|
+ // Tables
|
|
|
+ h = h.replace(/^(\|.+\|)\n(\|[-| :]+\|)\n((?:\|.+\|\n?)+)/gm, (_, header, sep, body) => {
|
|
|
+ const ths = header.split('|').filter(c=>c.trim()).map(c=>'<th>'+c.trim()+'</th>').join('');
|
|
|
+ const rows = body.trim().split('\n').map(row => {
|
|
|
+ const tds = row.split('|').filter(c=>c.trim()).map(c=>'<td>'+c.trim()+'</td>').join('');
|
|
|
+ return '<tr>'+tds+'</tr>';
|
|
|
+ }).join('');
|
|
|
+ return '<table><thead><tr>'+ths+'</tr></thead><tbody>'+rows+'</tbody></table>';
|
|
|
+ });
|
|
|
+ // Unordered list
|
|
|
+ h = h.replace(/^- (.+)$/gm, '<li>$1</li>');
|
|
|
+ h = h.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
|
|
|
+ // Ordered list
|
|
|
+ h = h.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
|
|
|
+ // Paragraphs: double newline
|
|
|
+ h = h.replace(/\n\n/g, '</p><p>');
|
|
|
+ // Single newlines (not inside block elements)
|
|
|
+ h = h.replace(/\n/g, '<br>');
|
|
|
+ // Clean up
|
|
|
+ h = '<p>' + h + '</p>';
|
|
|
+ h = h.replace(/<p>\s*<(h[1-4]|hr|pre|ul|ol|table|blockquote)/g, '<$1');
|
|
|
+ h = h.replace(/<\/(h[1-4]|pre|ul|ol|table|blockquote)>\s*<\/p>/g, '</$1>');
|
|
|
+ h = h.replace(/<p>\s*<\/p>/g, '');
|
|
|
+ return h;
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Boot ────────────────────────────────────
|
|
|
+init();
|
|
|
+</script>
|
|
|
+</body>
|
|
|
+</html>
|