first commit
This commit is contained in:
18
src/extension.ts
Normal file
18
src/extension.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { LauncherViewProvider } from './launcherView';
|
||||
import { openPlecUpload } from './tools/plecUpload';
|
||||
import { openApplovinUpload } from './tools/applovinUpload';
|
||||
import { openBase64Scanner } from './tools/base64Scanner';
|
||||
import { openDailyUpdate } from './tools/dailyUpdate';
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('hplToolbox.openPlecUpload', () => openPlecUpload(context)),
|
||||
vscode.commands.registerCommand('hplToolbox.openApplovinUpload', () => openApplovinUpload(context)),
|
||||
vscode.commands.registerCommand('hplToolbox.openBase64Scanner', () => openBase64Scanner(context)),
|
||||
vscode.commands.registerCommand('hplToolbox.openDailyUpdate', () => openDailyUpdate(context)),
|
||||
vscode.window.registerWebviewViewProvider('hplToolbox.launcher', new LauncherViewProvider())
|
||||
);
|
||||
}
|
||||
|
||||
export function deactivate() {}
|
||||
65
src/launcherView.ts
Normal file
65
src/launcherView.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class LauncherViewProvider implements vscode.WebviewViewProvider {
|
||||
resolveWebviewView(view: vscode.WebviewView) {
|
||||
view.webview.options = { enableScripts: true };
|
||||
view.webview.html = getHtml();
|
||||
view.webview.onDidReceiveMessage((msg) => {
|
||||
if (msg?.type === 'open' && typeof msg.command === 'string') {
|
||||
vscode.commands.executeCommand(msg.command);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getHtml(): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<style>
|
||||
body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 12px 8px; }
|
||||
h3 { margin: 0 0 12px 0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; opacity: 0.7; }
|
||||
.tool-btn {
|
||||
display: block; width: 100%; text-align: left;
|
||||
padding: 8px 10px; margin-bottom: 6px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 3px; cursor: pointer; font-size: 13px;
|
||||
}
|
||||
.tool-btn:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
.tool-desc { display: block; font-size: 11px; opacity: 0.7; margin-top: 2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3>HPL Toolbox</h3>
|
||||
<button class="tool-btn" data-cmd="hplToolbox.openPlecUpload">
|
||||
PLEC Upload
|
||||
<span class="tool-desc">Upload HTML to internal PLEC server</span>
|
||||
</button>
|
||||
<button class="tool-btn" data-cmd="hplToolbox.openApplovinUpload">
|
||||
AppLovin Demo Upload
|
||||
<span class="tool-desc">Upload to p.applov.in (QR preview)</span>
|
||||
</button>
|
||||
<button class="tool-btn" data-cmd="hplToolbox.openBase64Scanner">
|
||||
Base64 Scanner
|
||||
<span class="tool-desc">Find non-base64 assets in HTML</span>
|
||||
</button>
|
||||
<button class="tool-btn" data-cmd="hplToolbox.openDailyUpdate">
|
||||
Daily Update
|
||||
<span class="tool-desc">Compose & copy a daily status</span>
|
||||
</button>
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
document.querySelectorAll('.tool-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
vscode.postMessage({ type: 'open', command: btn.dataset.cmd });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
262
src/tools/applovinUpload.ts
Normal file
262
src/tools/applovinUpload.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { File } from 'node:buffer';
|
||||
import { handleClipboardAndOpen, singletonPanel } from './shared';
|
||||
|
||||
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
|
||||
|
||||
export function openApplovinUpload(_context: vscode.ExtensionContext) {
|
||||
const { panel, isNew } = singletonPanel(store, 'hplToolbox.applovinUpload', 'AppLovin Demo Upload');
|
||||
if (!isNew) return;
|
||||
|
||||
panel.webview.html = getHtml();
|
||||
panel.webview.onDidReceiveMessage(async (msg) => {
|
||||
try {
|
||||
if (handleClipboardAndOpen(msg)) return;
|
||||
|
||||
if (msg.type === 'pickFiles') {
|
||||
const picked = await vscode.window.showOpenDialog({
|
||||
canSelectMany: true,
|
||||
filters: { HTML: ['html', 'htm'] },
|
||||
openLabel: 'Select',
|
||||
});
|
||||
if (picked && picked.length) {
|
||||
panel.webview.postMessage({
|
||||
type: 'filesSelected',
|
||||
files: picked.map(u => ({ path: u.fsPath, name: path.basename(u.fsPath) })),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'upload') {
|
||||
await handleUpload(panel, msg.paths);
|
||||
return;
|
||||
}
|
||||
} catch (err: any) {
|
||||
panel.webview.postMessage({ type: 'error', message: err?.message || String(err) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
|
||||
const cfg = vscode.workspace.getConfiguration('hplToolbox.applovin');
|
||||
const cookie = (cfg.get<string>('cookie') || '').trim();
|
||||
|
||||
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
|
||||
if (!paths.length) {
|
||||
panel.webview.postMessage({ type: 'error', message: 'No files selected.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'Origin': 'https://p.applov.in',
|
||||
'Referer': 'https://p.applov.in/playablePreview?create=1&qr=1',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'User-Agent': 'Mozilla/5.0 (HPL-Toolbox-VSCode)',
|
||||
};
|
||||
if (cookie) headers['Cookie'] = `__Host-APPLOVINID=${cookie}`;
|
||||
|
||||
const buildForm = (filePath: string): FormData => {
|
||||
const buf = fs.readFileSync(filePath);
|
||||
const file = new File([buf], path.basename(filePath), { type: 'text/html' });
|
||||
const form = new FormData();
|
||||
form.append('file', file as any);
|
||||
return form;
|
||||
};
|
||||
|
||||
panel.webview.postMessage({ type: 'start', total: paths.length });
|
||||
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const filePath = paths[i];
|
||||
const fileName = path.basename(filePath);
|
||||
const idx = i + 1;
|
||||
const reportError = (message: string) => {
|
||||
panel.webview.postMessage({ type: 'fileError', name: fileName, message });
|
||||
};
|
||||
|
||||
if (!filePath || !fs.existsSync(filePath)) { reportError('File missing'); continue; }
|
||||
if (!/\.html?$/i.test(filePath)) { reportError('Must be .html'); continue; }
|
||||
|
||||
panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Validating ${fileName}...` });
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch('https://p.applov.in/validateHTMLContent', {
|
||||
method: 'POST', body: buildForm(filePath), headers,
|
||||
});
|
||||
} catch (err: any) { reportError('Network error (validate): ' + (err?.message || err)); continue; }
|
||||
const validateText = await res.text();
|
||||
if (!res.ok) { reportError(`Validate HTTP ${res.status}: ${validateText.slice(0, 300)}`); continue; }
|
||||
let validateJson: any;
|
||||
try { validateJson = JSON.parse(validateText); } catch { validateJson = null; }
|
||||
if (validateJson && validateJson.code && validateJson.code !== 200) {
|
||||
reportError(`Validation failed: ${validateText.slice(0, 300)}`); continue;
|
||||
}
|
||||
|
||||
panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Uploading ${fileName}...` });
|
||||
|
||||
try {
|
||||
res = await fetch('https://p.applov.in/getCachedAdURL', {
|
||||
method: 'POST', body: buildForm(filePath), headers,
|
||||
});
|
||||
} catch (err: any) { reportError('Network error (cache): ' + (err?.message || err)); continue; }
|
||||
const cacheText = await res.text();
|
||||
if (!res.ok) { reportError(`Cache HTTP ${res.status}: ${cacheText.slice(0, 300)}`); continue; }
|
||||
let cacheJson: any;
|
||||
try { cacheJson = JSON.parse(cacheText); }
|
||||
catch { reportError(`Non-JSON response: ${cacheText.slice(0, 300)}`); continue; }
|
||||
const hash = cacheJson.results;
|
||||
if (!hash) { reportError(`No hash in response: ${cacheText.slice(0, 300)}`); continue; }
|
||||
|
||||
panel.webview.postMessage({
|
||||
type: 'fileResult',
|
||||
name: fileName,
|
||||
hash,
|
||||
previewUrl: `https://p.applov.in/getPreviewHTML?preview=${hash}`,
|
||||
pageUrl: `https://p.applov.in/playablePreview?preview=${hash}&qr=1`,
|
||||
qrImg: `https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${encodeURIComponent(hash)}`,
|
||||
});
|
||||
}
|
||||
|
||||
panel.webview.postMessage({ type: 'done' });
|
||||
}
|
||||
|
||||
function getHtml(): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<style>
|
||||
body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 16px; }
|
||||
h2 { margin-top: 0; }
|
||||
button {
|
||||
background: var(--vscode-button-background); color: var(--vscode-button-foreground);
|
||||
border: none; padding: 6px 12px; border-radius: 2px; cursor: pointer;
|
||||
}
|
||||
button:hover { background: var(--vscode-button-hoverBackground); }
|
||||
button.secondary {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
button.secondary:hover { background: var(--vscode-button-secondaryHoverBackground); }
|
||||
.file-cell { display: flex; align-items: center; gap: 8px; }
|
||||
.file-name { opacity: 0.85; font-size: 12px; word-break: break-all; }
|
||||
.actions { margin-top: 12px; display: flex; gap: 8px; }
|
||||
#status { margin-top: 12px; min-height: 20px; }
|
||||
.err { color: var(--vscode-errorForeground); white-space: pre-wrap; }
|
||||
.ok { color: var(--vscode-testing-iconPassed, var(--vscode-foreground)); }
|
||||
.result {
|
||||
padding: 10px;
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-left: 3px solid var(--vscode-textBlockQuote-border);
|
||||
word-break: break-all;
|
||||
}
|
||||
.result-actions { margin-top: 8px; display: flex; gap: 8px; }
|
||||
#results { margin-top: 12px; display: grid; grid-template-columns: repeat(auto-fill,minmax(280px,1fr)); gap: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>AppLovin Playable Preview (QR)</h2>
|
||||
<p style="opacity:0.8;font-size:12px;margin-top:0;">Upload HTML files to p.applov.in and get QR codes to scan with the AppLovin Playable Preview app (iOS/Android). Requires the <code>__Host-APPLOVINID</code> cookie set in <em>HPL Toolbox — AppLovin Upload</em> settings.</p>
|
||||
|
||||
<div class="file-cell" style="margin-bottom:8px;">
|
||||
<button id="pick" class="secondary">Choose files...</button>
|
||||
<span class="file-name" id="fileName">(no files)</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="upload">Upload to AppLovin</button>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
<div id="results"></div>
|
||||
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
const pickBtn = document.getElementById('pick');
|
||||
const uploadBtn = document.getElementById('upload');
|
||||
const fileNameEl = document.getElementById('fileName');
|
||||
const statusEl = document.getElementById('status');
|
||||
const resultsEl = document.getElementById('results');
|
||||
let selectedPaths = [];
|
||||
|
||||
pickBtn.addEventListener('click', () => vscode.postMessage({ type: 'pickFiles' }));
|
||||
|
||||
uploadBtn.addEventListener('click', () => {
|
||||
statusEl.textContent = '';
|
||||
resultsEl.innerHTML = '';
|
||||
if (!selectedPaths.length) {
|
||||
statusEl.innerHTML = '<span class="err">Pick at least one HTML file first.</span>';
|
||||
return;
|
||||
}
|
||||
uploadBtn.disabled = true;
|
||||
vscode.postMessage({ type: 'upload', paths: selectedPaths });
|
||||
});
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
const m = e.data;
|
||||
if (m.type === 'filesSelected') {
|
||||
selectedPaths = m.files.map(f => f.path);
|
||||
const names = m.files.map(f => f.name);
|
||||
fileNameEl.textContent = names.length === 1
|
||||
? names[0]
|
||||
: names.length + ' files: ' + names.slice(0, 3).join(', ') + (names.length > 3 ? '...' : '');
|
||||
} else if (m.type === 'start') {
|
||||
resultsEl.innerHTML = '';
|
||||
} else if (m.type === 'status') {
|
||||
statusEl.textContent = m.message;
|
||||
statusEl.className = '';
|
||||
} else if (m.type === 'error') {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'err';
|
||||
div.textContent = m.message;
|
||||
statusEl.innerHTML = '';
|
||||
statusEl.appendChild(div);
|
||||
uploadBtn.disabled = false;
|
||||
} else if (m.type === 'fileError') {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'result';
|
||||
wrap.innerHTML = '<div><strong>' + escapeHtml(m.name) + '</strong></div>' +
|
||||
'<div class="err" style="margin-top:6px;">' + escapeHtml(m.message) + '</div>';
|
||||
resultsEl.appendChild(wrap);
|
||||
} else if (m.type === 'fileResult') {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'result';
|
||||
wrap.innerHTML =
|
||||
'<div style="font-weight:600;margin-bottom:8px;word-break:break-all;">' + escapeHtml(m.name) + '</div>' +
|
||||
'<div style="text-align:center;margin-bottom:8px;">' +
|
||||
'<img src="' + m.qrImg + '" alt="QR" style="background:#fff;padding:8px;border-radius:4px;max-width:100%;" />' +
|
||||
'</div>' +
|
||||
'<div style="font-size:11px;opacity:0.8;word-break:break-all;">Hash: ' + escapeHtml(m.hash) + '</div>';
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'result-actions';
|
||||
const copyHash = document.createElement('button');
|
||||
copyHash.textContent = 'Copy Hash';
|
||||
copyHash.onclick = () => vscode.postMessage({ type: 'copy', text: m.hash });
|
||||
const copyPreview = document.createElement('button');
|
||||
copyPreview.textContent = 'Copy URL';
|
||||
copyPreview.className = 'secondary';
|
||||
copyPreview.onclick = () => vscode.postMessage({ type: 'copy', text: m.previewUrl });
|
||||
const openPreview = document.createElement('button');
|
||||
openPreview.textContent = 'Open';
|
||||
openPreview.className = 'secondary';
|
||||
openPreview.onclick = () => vscode.postMessage({ type: 'open', text: m.previewUrl });
|
||||
actions.appendChild(copyHash);
|
||||
actions.appendChild(copyPreview);
|
||||
actions.appendChild(openPreview);
|
||||
wrap.appendChild(actions);
|
||||
resultsEl.appendChild(wrap);
|
||||
} else if (m.type === 'done') {
|
||||
statusEl.innerHTML = '<span class="ok">Done.</span>';
|
||||
uploadBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
192
src/tools/base64Scanner.ts
Normal file
192
src/tools/base64Scanner.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { singletonPanel } from './shared';
|
||||
|
||||
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
|
||||
|
||||
export function openBase64Scanner(_context: vscode.ExtensionContext) {
|
||||
const { panel, isNew } = singletonPanel(store, 'hplToolbox.base64Scanner', 'Base64 Asset Scanner');
|
||||
if (!isNew) return;
|
||||
|
||||
panel.webview.html = getHtml();
|
||||
panel.webview.onDidReceiveMessage(async (msg) => {
|
||||
if (msg.type === 'ready') {
|
||||
const ws = vscode.workspace.workspaceFolders?.[0];
|
||||
if (ws) {
|
||||
const distPath = path.join(ws.uri.fsPath, 'dist');
|
||||
if (fs.existsSync(distPath) && fs.statSync(distPath).isDirectory()) {
|
||||
panel.webview.postMessage({ type: 'folderPicked', path: distPath });
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'pickFolder') {
|
||||
const picked = await vscode.window.showOpenDialog({
|
||||
canSelectFolders: true,
|
||||
canSelectFiles: false,
|
||||
canSelectMany: false,
|
||||
openLabel: 'Select Folder',
|
||||
});
|
||||
if (picked && picked[0]) {
|
||||
panel.webview.postMessage({ type: 'folderPicked', path: picked[0].fsPath });
|
||||
}
|
||||
} else if (msg.type === 'scan') {
|
||||
const folder: string = msg.folder;
|
||||
if (!folder || !fs.existsSync(folder)) {
|
||||
panel.webview.postMessage({ type: 'results', results: [], error: 'Invalid folder' });
|
||||
return;
|
||||
}
|
||||
const htmlFiles = await collectHtmlFiles(folder);
|
||||
const results: { file: string; ok: boolean; assets: string[] }[] = [];
|
||||
for (const file of htmlFiles) {
|
||||
try {
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
const offenders = findNonBase64Assets(content);
|
||||
results.push({ file: path.relative(folder, file), ok: offenders.length === 0, assets: offenders });
|
||||
} catch {
|
||||
// skip unreadable
|
||||
}
|
||||
}
|
||||
panel.webview.postMessage({ type: 'results', results });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function collectHtmlFiles(root: string): Promise<string[]> {
|
||||
const out: string[] = [];
|
||||
const stack: string[] = [root];
|
||||
while (stack.length) {
|
||||
const dir = stack.pop()!;
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const e of entries) {
|
||||
const full = path.join(dir, e.name);
|
||||
if (e.isDirectory()) {
|
||||
if (e.name === 'node_modules' || e.name === '.git') continue;
|
||||
stack.push(full);
|
||||
} else if (e.isFile() && /\.html?$/i.test(e.name)) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function findNonBase64Assets(html: string): string[] {
|
||||
const markup = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
||||
const offenders = new Set<string>();
|
||||
const attrRegex = /\b(?:src|href|poster|data-src)\s*=\s*("([^"]*)"|'([^']*)')/gi;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = attrRegex.exec(markup)) !== null) {
|
||||
const url = (m[2] ?? m[3] ?? '').trim();
|
||||
if (!url) continue;
|
||||
if (isAssetReference(url) && !isBase64DataUri(url)) {
|
||||
offenders.add(url);
|
||||
}
|
||||
}
|
||||
const cssUrlRegex = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*))\s*\)/gi;
|
||||
while ((m = cssUrlRegex.exec(markup)) !== null) {
|
||||
const url = (m[1] ?? m[2] ?? m[3] ?? '').trim();
|
||||
if (!url) continue;
|
||||
if (!isBase64DataUri(url)) {
|
||||
offenders.add(url);
|
||||
}
|
||||
}
|
||||
return [...offenders];
|
||||
}
|
||||
|
||||
function isBase64DataUri(url: string): boolean {
|
||||
return /^data:[^;,]*;base64,/i.test(url);
|
||||
}
|
||||
|
||||
function isAssetReference(url: string): boolean {
|
||||
if (url.startsWith('#') || url.toLowerCase().startsWith('javascript:')) return false;
|
||||
if (/^(mailto:|tel:)/i.test(url)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function getHtml(): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<style>
|
||||
body { font-family: var(--vscode-font-family); padding: 12px; color: var(--vscode-foreground); }
|
||||
.row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
|
||||
input[type=text] { flex: 1; padding: 4px 6px; background: var(--vscode-input-background); color: var(--vscode-input-foreground); border: 1px solid var(--vscode-input-border); }
|
||||
button { padding: 4px 12px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; cursor: pointer; }
|
||||
button:hover { background: var(--vscode-button-hoverBackground); }
|
||||
.row-result { display: flex; gap: 8px; padding: 2px 0; font-family: var(--vscode-editor-font-family); font-size: 12px; }
|
||||
.mark { width: 14px; flex-shrink: 0; font-weight: bold; }
|
||||
.ok { color: #3fb950; }
|
||||
.bad { color: #f85149; }
|
||||
.file-name { word-break: break-all; }
|
||||
.assets { color: var(--vscode-descriptionForeground); margin-left: 6px; word-break: break-all; }
|
||||
.summary { margin-top: 12px; opacity: 0.8; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Base64 Asset Scanner</h2>
|
||||
<div class="row">
|
||||
<input id="folder" type="text" placeholder="Folder to scan recursively..." />
|
||||
<button id="pick">Browse...</button>
|
||||
<button id="scan">Scan</button>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
<div id="results"></div>
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
vscode.postMessage({ type: 'ready' });
|
||||
const folderEl = document.getElementById('folder');
|
||||
const statusEl = document.getElementById('status');
|
||||
const resultsEl = document.getElementById('results');
|
||||
|
||||
document.getElementById('pick').addEventListener('click', () => {
|
||||
vscode.postMessage({ type: 'pickFolder' });
|
||||
});
|
||||
document.getElementById('scan').addEventListener('click', () => {
|
||||
const folder = folderEl.value.trim();
|
||||
if (!folder) { statusEl.textContent = 'Pick a folder first.'; return; }
|
||||
statusEl.textContent = 'Scanning...';
|
||||
resultsEl.innerHTML = '';
|
||||
vscode.postMessage({ type: 'scan', folder });
|
||||
});
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const msg = event.data;
|
||||
if (msg.type === 'folderPicked') {
|
||||
folderEl.value = msg.path;
|
||||
} else if (msg.type === 'results') {
|
||||
if (msg.error) { statusEl.textContent = 'Error: ' + msg.error; return; }
|
||||
const total = msg.results.length;
|
||||
const flagged = msg.results.filter(r => !r.ok).length;
|
||||
statusEl.innerHTML = '<div class="summary">Scanned ' + total + ' HTML file(s). ' + flagged + ' contain non-base64 assets.</div>';
|
||||
resultsEl.innerHTML = '';
|
||||
for (const r of msg.results) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row-result';
|
||||
const mark = document.createElement('span');
|
||||
mark.className = 'mark ' + (r.ok ? 'ok' : 'bad');
|
||||
mark.textContent = r.ok ? '✓' : '✗';
|
||||
const name = document.createElement('span');
|
||||
name.className = 'file-name';
|
||||
name.textContent = r.file;
|
||||
row.appendChild(mark);
|
||||
row.appendChild(name);
|
||||
if (!r.ok) {
|
||||
const a = document.createElement('span');
|
||||
a.className = 'assets';
|
||||
a.textContent = '— ' + r.assets.join(', ');
|
||||
row.appendChild(a);
|
||||
}
|
||||
resultsEl.appendChild(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
121
src/tools/dailyUpdate.ts
Normal file
121
src/tools/dailyUpdate.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { singletonPanel } from './shared';
|
||||
|
||||
const PROJECT_KEY = 'hplToolbox.dailyUpdate.lastProject';
|
||||
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
|
||||
|
||||
export function openDailyUpdate(context: vscode.ExtensionContext) {
|
||||
const { panel, isNew } = singletonPanel(store, 'hplToolbox.dailyUpdate', 'Daily Update');
|
||||
if (!isNew) return;
|
||||
|
||||
const lastProject = context.globalState.get<string>(PROJECT_KEY, '');
|
||||
panel.webview.html = getHtml(lastProject);
|
||||
|
||||
panel.webview.onDidReceiveMessage(async (msg) => {
|
||||
if (msg.type === 'submit') {
|
||||
const text = formatMessage(msg.payload);
|
||||
await vscode.env.clipboard.writeText(text);
|
||||
await context.globalState.update(PROJECT_KEY, msg.payload.project);
|
||||
vscode.window.showInformationMessage('Daily update copied to clipboard.');
|
||||
panel.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatMessage(p: { date: string; status: string; project: string; remarks: string }): string {
|
||||
const bullets = p.remarks
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.replace(/^\s*[-*]\s*/, '').trim())
|
||||
.filter((l) => l.length > 0)
|
||||
.map((l) => `- ${l}`)
|
||||
.join('\n');
|
||||
|
||||
const lines = [
|
||||
`Date: ${formatDate(p.date)}`,
|
||||
`Status: ${p.status}`,
|
||||
`Project: ${p.project}`,
|
||||
`Remarks:`,
|
||||
bullets,
|
||||
];
|
||||
|
||||
if (p.status === 'For OT') {
|
||||
lines.push('@Joyce Lanot , @Reland Pigte, @Anthony Castor, @Angela Grace');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
||||
if (!m) return iso;
|
||||
return `${m[2]}/${m[3]}/${m[1]}`;
|
||||
}
|
||||
|
||||
function escapeAttr(s: string): string {
|
||||
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function getHtml(lastProject: string): string {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline';" />
|
||||
<style>
|
||||
body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 16px; max-width: 520px; }
|
||||
label { display: block; margin-top: 12px; font-size: 12px; opacity: 0.85; }
|
||||
input, select, textarea {
|
||||
width: 100%; box-sizing: border-box; margin-top: 4px; padding: 6px 8px;
|
||||
background: var(--vscode-input-background); color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border, transparent); font-family: inherit; font-size: 13px;
|
||||
}
|
||||
textarea { min-height: 96px; resize: vertical; }
|
||||
button {
|
||||
margin-top: 16px; padding: 8px 16px; cursor: pointer;
|
||||
background: var(--vscode-button-background); color: var(--vscode-button-foreground);
|
||||
border: none; font-size: 13px;
|
||||
}
|
||||
button:hover { background: var(--vscode-button-hoverBackground); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2 style="margin:0 0 4px 0;">Daily Update</h2>
|
||||
<div style="font-size:12px;opacity:0.7;">Submit copies the formatted text to your clipboard.</div>
|
||||
|
||||
<label for="date">Date</label>
|
||||
<input type="date" id="date" value="${today}" />
|
||||
|
||||
<label for="status">Status</label>
|
||||
<select id="status">
|
||||
<option>Started</option>
|
||||
<option>Progress</option>
|
||||
<option>Finished</option>
|
||||
<option>For OT</option>
|
||||
</select>
|
||||
|
||||
<label for="project">Project</label>
|
||||
<input type="text" id="project" placeholder="Project name" value="${escapeAttr(lastProject)}" />
|
||||
|
||||
<label for="remarks">Remarks</label>
|
||||
<textarea id="remarks" placeholder="What did you do?"></textarea>
|
||||
|
||||
<button id="submit">Submit</button>
|
||||
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
document.getElementById('submit').addEventListener('click', () => {
|
||||
vscode.postMessage({
|
||||
type: 'submit',
|
||||
payload: {
|
||||
date: document.getElementById('date').value,
|
||||
status: document.getElementById('status').value,
|
||||
project: document.getElementById('project').value.trim(),
|
||||
remarks: document.getElementById('remarks').value.trim(),
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
347
src/tools/plecUpload.ts
Normal file
347
src/tools/plecUpload.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { File } from 'node:buffer';
|
||||
import { handleClipboardAndOpen, singletonPanel } from './shared';
|
||||
|
||||
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
|
||||
|
||||
export function openPlecUpload(_context: vscode.ExtensionContext) {
|
||||
const { panel, isNew } = singletonPanel(store, 'hplToolbox.plecUpload', 'PLEC Upload');
|
||||
if (!isNew) return;
|
||||
|
||||
panel.webview.html = getHtml();
|
||||
panel.webview.onDidReceiveMessage(async (msg) => {
|
||||
try {
|
||||
if (handleClipboardAndOpen(msg)) return;
|
||||
|
||||
if (msg.type === 'pickFile') {
|
||||
const picked = await vscode.window.showOpenDialog({
|
||||
canSelectMany: false,
|
||||
filters: { HTML: ['html', 'htm'] },
|
||||
openLabel: 'Select',
|
||||
});
|
||||
if (picked && picked[0]) {
|
||||
const fp = picked[0].fsPath;
|
||||
panel.webview.postMessage({
|
||||
type: 'fileSelected',
|
||||
rowId: msg.rowId,
|
||||
path: fp,
|
||||
name: path.basename(fp),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'upload') {
|
||||
await handleUpload(panel, msg.rows);
|
||||
return;
|
||||
}
|
||||
} catch (err: any) {
|
||||
panel.webview.postMessage({ type: 'error', message: err?.message || String(err) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
interface UploadRow {
|
||||
id: string;
|
||||
path: string;
|
||||
iteration: string;
|
||||
}
|
||||
|
||||
async function handleUpload(panel: vscode.WebviewPanel, rows: UploadRow[]) {
|
||||
const cfg = vscode.workspace.getConfiguration('hplToolbox.plec');
|
||||
const uploadUrl = cfg.get<string>('uploadUrl')!;
|
||||
const originUrl = cfg.get<string>('originUrl')!;
|
||||
const previewBase = cfg.get<string>('previewBase')!;
|
||||
const skipExistsCheck = cfg.get<boolean>('skipExistsCheck')!;
|
||||
|
||||
const valid: UploadRow[] = [];
|
||||
for (const r of rows) {
|
||||
if (!r.path || !fs.existsSync(r.path)) {
|
||||
panel.webview.postMessage({ type: 'rowError', rowId: r.id, message: 'File missing' });
|
||||
return;
|
||||
}
|
||||
if (!r.iteration || !r.iteration.trim()) {
|
||||
panel.webview.postMessage({ type: 'rowError', rowId: r.id, message: 'Iteration name required' });
|
||||
return;
|
||||
}
|
||||
if (!/\.html?$/i.test(r.path)) {
|
||||
panel.webview.postMessage({ type: 'rowError', rowId: r.id, message: 'Must be .html' });
|
||||
return;
|
||||
}
|
||||
valid.push(r);
|
||||
}
|
||||
if (valid.length === 0) {
|
||||
panel.webview.postMessage({ type: 'error', message: 'Nothing to upload.' });
|
||||
return;
|
||||
}
|
||||
|
||||
panel.webview.postMessage({ type: 'status', message: 'Checking filename availability...' });
|
||||
|
||||
if (!skipExistsCheck) {
|
||||
for (const r of valid) {
|
||||
const fname = path.basename(r.path);
|
||||
const exists = await checkFileExists(previewBase + encodeURIComponent(fname));
|
||||
if (exists === true) {
|
||||
panel.webview.postMessage({
|
||||
type: 'rowError',
|
||||
rowId: r.id,
|
||||
message: `Filename "${fname}" already exists on preview server. Rename and retry.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
panel.webview.postMessage({ type: 'status', message: 'Uploading...' });
|
||||
|
||||
const form = new FormData();
|
||||
for (let i = 0; i < valid.length; i++) {
|
||||
const r = valid[i];
|
||||
const idx = i + 1;
|
||||
const buf = fs.readFileSync(r.path);
|
||||
const file = new File([buf], path.basename(r.path), { type: 'text/html' });
|
||||
form.append(`fileUpload_${idx}`, file as any);
|
||||
form.append(`iterationNameUpload_${idx}`, encodeURI(r.iteration.replace(/\s+/g, '')));
|
||||
}
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: {
|
||||
'Origin': originUrl,
|
||||
'Referer': originUrl + '/',
|
||||
'User-Agent': 'Mozilla/5.0 (HPL-Toolbox-VSCode)',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': '*/*',
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
panel.webview.postMessage({ type: 'error', message: 'Network error: ' + (err?.message || err) });
|
||||
return;
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
panel.webview.postMessage({
|
||||
type: 'error',
|
||||
message: `HTTP ${res.status}\n${text.slice(0, 800)}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let json: any;
|
||||
try { json = JSON.parse(text); }
|
||||
catch {
|
||||
panel.webview.postMessage({
|
||||
type: 'error',
|
||||
message: `Server returned non-JSON:\n${text.slice(0, 800)}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const preview = json.preview || json.previewURL || null;
|
||||
panel.webview.postMessage({ type: 'result', preview, raw: json, rawText: text });
|
||||
}
|
||||
|
||||
async function checkFileExists(url: string): Promise<boolean | null> {
|
||||
try {
|
||||
const res = await fetch(url, { method: 'HEAD' });
|
||||
if (res.status === 200) return true;
|
||||
if (res.status === 404) return false;
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getHtml(): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<style>
|
||||
body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 16px; }
|
||||
h2 { margin-top: 0; }
|
||||
table { width: 100%; border-collapse: collapse; margin-bottom: 12px; }
|
||||
th, td { padding: 6px 8px; text-align: left; vertical-align: middle; border-bottom: 1px solid var(--vscode-panel-border); }
|
||||
th { font-weight: 600; font-size: 12px; text-transform: uppercase; opacity: 0.7; }
|
||||
input[type=text] {
|
||||
width: 100%; padding: 4px 6px;
|
||||
background: var(--vscode-input-background); color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border, transparent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
button {
|
||||
background: var(--vscode-button-background); color: var(--vscode-button-foreground);
|
||||
border: none; padding: 6px 12px; border-radius: 2px; cursor: pointer;
|
||||
}
|
||||
button:hover { background: var(--vscode-button-hoverBackground); }
|
||||
button.secondary {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
button.secondary:hover { background: var(--vscode-button-secondaryHoverBackground); }
|
||||
.file-cell { display: flex; align-items: center; gap: 8px; }
|
||||
.file-name { opacity: 0.85; font-size: 12px; word-break: break-all; }
|
||||
.row-error { color: var(--vscode-errorForeground); font-size: 12px; margin-top: 4px; }
|
||||
.actions { margin-top: 12px; display: flex; gap: 8px; }
|
||||
#status { margin-top: 12px; min-height: 20px; }
|
||||
.err { color: var(--vscode-errorForeground); white-space: pre-wrap; }
|
||||
.ok { color: var(--vscode-testing-iconPassed, var(--vscode-foreground)); }
|
||||
.result {
|
||||
margin-top: 12px; padding: 10px;
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-left: 3px solid var(--vscode-textBlockQuote-border);
|
||||
word-break: break-all;
|
||||
}
|
||||
.result a { color: var(--vscode-textLink-foreground); }
|
||||
.result-actions { margin-top: 8px; display: flex; gap: 8px; }
|
||||
.remove-btn { background: transparent; color: var(--vscode-foreground); padding: 4px 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>PLEC Upload</h2>
|
||||
<p style="opacity:0.8;font-size:12px;margin-top:0;">Pick HTML files, give each an iteration name, then upload. The preview link is returned by the server.</p>
|
||||
|
||||
<table id="rows">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:45%">HTML File</th>
|
||||
<th style="width:45%">Iteration Name</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<div class="actions">
|
||||
<button id="addRow" class="secondary">+ Add Row</button>
|
||||
<button id="upload">Upload</button>
|
||||
</div>
|
||||
|
||||
<div id="status"></div>
|
||||
<div id="results"></div>
|
||||
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
let nextId = 1;
|
||||
const tbody = document.querySelector('#rows tbody');
|
||||
const statusEl = document.getElementById('status');
|
||||
const resultsEl = document.getElementById('results');
|
||||
|
||||
function addRow() {
|
||||
const id = 'r' + (nextId++);
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.id = id;
|
||||
tr.innerHTML = \`
|
||||
<td>
|
||||
<div class="file-cell">
|
||||
<button class="pick secondary">Choose...</button>
|
||||
<span class="file-name" data-name>(no file)</span>
|
||||
</div>
|
||||
<input type="hidden" data-path />
|
||||
<div class="row-error" data-error></div>
|
||||
</td>
|
||||
<td><input type="text" data-iter placeholder="iteration name" /></td>
|
||||
<td><button class="remove-btn" title="Remove">×</button></td>
|
||||
\`;
|
||||
tr.querySelector('.pick').addEventListener('click', () => {
|
||||
vscode.postMessage({ type: 'pickFile', rowId: id });
|
||||
});
|
||||
tr.querySelector('.remove-btn').addEventListener('click', () => {
|
||||
tr.remove();
|
||||
if (!tbody.children.length) addRow();
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
document.getElementById('addRow').addEventListener('click', addRow);
|
||||
|
||||
document.getElementById('upload').addEventListener('click', () => {
|
||||
clearErrors();
|
||||
statusEl.textContent = '';
|
||||
resultsEl.innerHTML = '';
|
||||
const rows = [...tbody.querySelectorAll('tr')].map(tr => ({
|
||||
id: tr.dataset.id,
|
||||
path: tr.querySelector('[data-path]').value,
|
||||
iteration: tr.querySelector('[data-iter]').value,
|
||||
})).filter(r => r.path || r.iteration);
|
||||
if (rows.length === 0) {
|
||||
statusEl.innerHTML = '<span class="err">Add at least one row with a file and iteration name.</span>';
|
||||
return;
|
||||
}
|
||||
vscode.postMessage({ type: 'upload', rows });
|
||||
});
|
||||
|
||||
function clearErrors() {
|
||||
document.querySelectorAll('[data-error]').forEach(el => el.textContent = '');
|
||||
}
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
const m = e.data;
|
||||
if (m.type === 'fileSelected') {
|
||||
const tr = tbody.querySelector('tr[data-id="' + m.rowId + '"]');
|
||||
if (!tr) return;
|
||||
tr.querySelector('[data-path]').value = m.path;
|
||||
tr.querySelector('[data-name]').textContent = m.name;
|
||||
const iter = tr.querySelector('[data-iter]');
|
||||
if (!iter.value) iter.value = m.name.replace(/\\.html?$/i, '');
|
||||
} else if (m.type === 'status') {
|
||||
statusEl.textContent = m.message;
|
||||
statusEl.className = '';
|
||||
} else if (m.type === 'error') {
|
||||
statusEl.innerHTML = '';
|
||||
const div = document.createElement('div');
|
||||
div.className = 'err';
|
||||
div.textContent = m.message;
|
||||
statusEl.appendChild(div);
|
||||
} else if (m.type === 'rowError') {
|
||||
const tr = tbody.querySelector('tr[data-id="' + m.rowId + '"]');
|
||||
if (tr) tr.querySelector('[data-error]').textContent = m.message;
|
||||
statusEl.innerHTML = '<span class="err">Fix row errors and retry.</span>';
|
||||
} else if (m.type === 'result') {
|
||||
statusEl.innerHTML = '<span class="ok">Upload complete.</span>';
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'result';
|
||||
if (m.preview) {
|
||||
wrap.innerHTML = '<div><strong>Preview:</strong> <a href="' + m.preview + '">' + m.preview + '</a></div>';
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'result-actions';
|
||||
const copy = document.createElement('button');
|
||||
copy.textContent = 'Copy';
|
||||
copy.onclick = () => vscode.postMessage({ type: 'copy', text: m.preview });
|
||||
const open = document.createElement('button');
|
||||
open.textContent = 'Open';
|
||||
open.className = 'secondary';
|
||||
open.onclick = () => vscode.postMessage({ type: 'open', text: m.preview });
|
||||
const showRaw = document.createElement('button');
|
||||
showRaw.textContent = 'Show server response';
|
||||
showRaw.className = 'secondary';
|
||||
showRaw.onclick = () => {
|
||||
const pre = document.createElement('pre');
|
||||
pre.style.marginTop = '8px';
|
||||
pre.style.whiteSpace = 'pre-wrap';
|
||||
pre.textContent = m.rawText || JSON.stringify(m.raw, null, 2);
|
||||
wrap.appendChild(pre);
|
||||
showRaw.disabled = true;
|
||||
};
|
||||
actions.appendChild(copy); actions.appendChild(open); actions.appendChild(showRaw);
|
||||
wrap.appendChild(actions);
|
||||
} else {
|
||||
wrap.innerHTML = '<div>No preview field in response. Raw JSON:</div><pre>' +
|
||||
(m.rawText || JSON.stringify(m.raw, null, 2)).replace(/</g, '<') + '</pre>';
|
||||
}
|
||||
resultsEl.innerHTML = '';
|
||||
resultsEl.appendChild(wrap);
|
||||
}
|
||||
});
|
||||
|
||||
addRow();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
34
src/tools/shared.ts
Normal file
34
src/tools/shared.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export function handleClipboardAndOpen(msg: any): boolean {
|
||||
if (msg?.type === 'copy' && typeof msg.text === 'string') {
|
||||
vscode.env.clipboard.writeText(msg.text);
|
||||
vscode.window.showInformationMessage('Copied to clipboard.');
|
||||
return true;
|
||||
}
|
||||
if (msg?.type === 'open' && typeof msg.text === 'string') {
|
||||
vscode.env.openExternal(vscode.Uri.parse(msg.text));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function singletonPanel(
|
||||
store: { panel: vscode.WebviewPanel | null },
|
||||
viewType: string,
|
||||
title: string
|
||||
): { panel: vscode.WebviewPanel; isNew: boolean } {
|
||||
if (store.panel) {
|
||||
store.panel.reveal();
|
||||
return { panel: store.panel, isNew: false };
|
||||
}
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
viewType,
|
||||
title,
|
||||
vscode.ViewColumn.Active,
|
||||
{ enableScripts: true, retainContextWhenHidden: true }
|
||||
);
|
||||
store.panel = panel;
|
||||
panel.onDidDispose(() => { store.panel = null; });
|
||||
return { panel, isNew: true };
|
||||
}
|
||||
Reference in New Issue
Block a user