first commit
This commit is contained in:
144
.gitignore
vendored
Normal file
144
.gitignore
vendored
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/node
|
||||||
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Run Extension",
|
||||||
|
"type": "extensionHost",
|
||||||
|
"request": "launch",
|
||||||
|
"args": [
|
||||||
|
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||||
|
],
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/out/**/*.js"
|
||||||
|
],
|
||||||
|
"preLaunchTask": "npm: compile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
19
.vscode/tasks.json
vendored
Normal file
19
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "compile",
|
||||||
|
"group": { "kind": "build", "isDefault": true },
|
||||||
|
"problemMatcher": ["$tsc"],
|
||||||
|
"label": "npm: compile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "watch",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": ["$tsc-watch"],
|
||||||
|
"label": "npm: watch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
hpl-toolbox-0.1.0.vsix
Normal file
BIN
hpl-toolbox-0.1.0.vsix
Normal file
Binary file not shown.
BIN
media/hpl.png
Normal file
BIN
media/hpl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
23
media/hpl.svg
Normal file
23
media/hpl.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 20 KiB |
1
media/icon.svg
Normal file
1
media/icon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M6 1.5L4.5 3H2.5l-.5.5v3l.5.5H4v6.5l.5.5H8v-1H5V7h1.5L7 6.5v-3L6.5 3H5l1-1h1.5v-1H6zm-3 4h3v-2H3v2zm9.5 9.5l-.85.35-2-2L10 13l.35-.85 2-2L13 10l-.65.65L11 11.999l1.35 1.35L13 14l-.5 1zm2.5-4.5h-1V8.5L13 8H9.5L9 8.5V13l.5.5H11v-1H10V9h3v1h1V8h-1V6H9V5h6v5z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 394 B |
58
package-lock.json
generated
Normal file
58
package-lock.json
generated
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "hpl-toolbox",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "hpl-toolbox",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/vscode": "^1.85.0",
|
||||||
|
"typescript": "^5.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.85.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.39",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
|
||||||
|
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/vscode": {
|
||||||
|
"version": "1.118.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.118.0.tgz",
|
||||||
|
"integrity": "sha512-Ah6eTlqDcwIMELEVwQMO++rJAFBRz/oLluLD/vWdYrH1KuI9kfpaM+7pg0OvvascgcJy+ghLCERAYouM4QbzGw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
package.json
Normal file
99
package.json
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
{
|
||||||
|
"name": "hpl-toolbox",
|
||||||
|
"displayName": "HPL Toolbox",
|
||||||
|
"description": "Bundled tools: PLEC Upload, AppLovin Demo Upload, Base64 Asset Scanner, Daily Update.",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"publisher": "hesukastro",
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.85.0"
|
||||||
|
},
|
||||||
|
"categories": ["Other"],
|
||||||
|
"icon": "media/hpl.png",
|
||||||
|
"main": "./out/extension.js",
|
||||||
|
"activationEvents": [],
|
||||||
|
"contributes": {
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "hplToolbox.openPlecUpload",
|
||||||
|
"title": "HPL Toolbox: Open PLEC Upload"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "hplToolbox.openApplovinUpload",
|
||||||
|
"title": "HPL Toolbox: Open AppLovin Demo Upload"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "hplToolbox.openBase64Scanner",
|
||||||
|
"title": "HPL Toolbox: Open Base64 Scanner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "hplToolbox.openDailyUpdate",
|
||||||
|
"title": "HPL Toolbox: Open Daily Update"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"viewsContainers": {
|
||||||
|
"activitybar": [
|
||||||
|
{
|
||||||
|
"id": "hplToolbox",
|
||||||
|
"title": "HPL Toolbox",
|
||||||
|
"icon": "media/hpl.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"hplToolbox": [
|
||||||
|
{
|
||||||
|
"type": "webview",
|
||||||
|
"id": "hplToolbox.launcher",
|
||||||
|
"name": "Tools"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configuration": [
|
||||||
|
{
|
||||||
|
"title": "HPL Toolbox — PLEC Upload",
|
||||||
|
"properties": {
|
||||||
|
"hplToolbox.plec.uploadUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "http://167.99.227.249/src/upload_HTML_Experimental.php",
|
||||||
|
"description": "Endpoint URL for the PLEC HTML upload script."
|
||||||
|
},
|
||||||
|
"hplToolbox.plec.originUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "http://167.99.227.249",
|
||||||
|
"description": "Origin/Referer header sent with upload requests."
|
||||||
|
},
|
||||||
|
"hplToolbox.plec.previewBase": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "https://playable.applovindemo.com/phaser/",
|
||||||
|
"description": "Preview server base URL used to check if a filename already exists."
|
||||||
|
},
|
||||||
|
"hplToolbox.plec.skipExistsCheck": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Skip the pre-check that verifies the filename does not already exist on the preview server."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "HPL Toolbox — AppLovin Upload",
|
||||||
|
"properties": {
|
||||||
|
"hplToolbox.applovin.cookie": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"description": "Value of the __Host-APPLOVINID cookie for p.applov.in (log in at https://p.applov.in/playablePreview?create=1&qr=1 and copy the cookie from DevTools)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"compile": "tsc -p ./",
|
||||||
|
"watch": "tsc -watch -p ./",
|
||||||
|
"vscode:prepublish": "npm run compile"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/vscode": "^1.85.0",
|
||||||
|
"typescript": "^5.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
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 };
|
||||||
|
}
|
||||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "ES2020",
|
||||||
|
"outDir": "out",
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
|
"sourceMap": true,
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", ".vscode-test"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user