feat: Allow the plugin to work under the browser context

Signed-off-by: Avior <f.bouillon@aptatio.com>
This commit is contained in:
Florian Bouillon 2023-01-20 12:14:00 +01:00
parent e42e5271db
commit cef14d1038
Signed by: Florian Bouillon
GPG Key ID: E05B3A94178D3A7C
10 changed files with 3603 additions and 233 deletions

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
.DS_Store .DS_Store
out out
node_modules node_modules
.vscode-test/ .vscode-test*/
*.vsix *.vsix

View File

@ -6,5 +6,8 @@
"search.exclude": { "search.exclude": {
"out": true // set this to false to include "out" folder in search results "out": true // set this to false to include "out" folder in search results
}, },
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib",
// username for internal browser tests
"codestats.username": "Aviortheking"
} }

14
esbuild.js Normal file
View File

@ -0,0 +1,14 @@
const { NodeModulesPolyfillPlugin } = require('@esbuild-plugins/node-modules-polyfill')
const { build } = require('esbuild')
build({
entryPoints: ['./src/code-stats.ts'],
plugins: [NodeModulesPolyfillPlugin()],
bundle: true,
minify: true,
sourcemap: true,
target: ['es2016', 'chrome90', 'firefox78', 'safari14', 'edge90'],
external: ['vscode', 'node-fetch'],
outfile: 'out/browser.js',
format: 'cjs',
platform: 'node'
})

3268
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,7 @@
"*" "*"
], ],
"main": "./out/src/code-stats", "main": "./out/src/code-stats",
"browser": "./out/browser",
"extensionKind": [ "extensionKind": [
"ui", "ui",
"workspace" "workspace"
@ -66,19 +67,25 @@
}, },
"scripts": { "scripts": {
"vscode:prepublish": "tsc -p ./", "vscode:prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./" "compile": "tsc -watch -p ./",
"test:browser": "vscode-test-web --extensionDevelopmentPath=. .",
"compile:browser": "node esbuild.js"
}, },
"devDependencies": { "devDependencies": {
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
"@types/mocha": "^7.0.2", "@types/mocha": "^7.0.2",
"@types/node": "^13.9.8", "@types/node": "^13.9.8",
"@types/node-fetch": "^2.6.2",
"@types/vscode": "^1.43.2", "@types/vscode": "^1.43.2",
"@vscode/test-web": "^0.0.34",
"esbuild": "^0.17.3",
"growl": "^1.10.5", "growl": "^1.10.5",
"mocha": "^7.1.1", "mocha": "^7.1.1",
"typescript": "^3.8.3", "typescript": "^3.8.3",
"vscode-test": "^1.5.2" "vscode-test": "^1.5.2"
}, },
"dependencies": { "dependencies": {
"axios": "^0.19.2", "lodash.template": "^4.5.0",
"lodash.template": "^4.5.0" "node-fetch": "^2.6.8"
} }
} }

View File

@ -1,13 +1,24 @@
import { Pulse } from "./pulse"; import type fetch from 'node-fetch'
import { getISOTimestamp, getLanguageName } from "./utils"; import { Pulse } from "./pulse"
import * as axios from "axios"; import { getISOTimestamp, getLanguageName } from "./utils"
// Please do not look at this
let realFetch: typeof fetch
// @ts-expect-error VSCode is not including fetch everytime
if (typeof fetch === 'undefined') {
realFetch = require('node-fetch')
} else {
// @ts-expect-error VSCode is not including fetch everytime
realFetch = fetch
}
export class CodeStatsAPI { export class CodeStatsAPI {
private API_KEY = null; private API_KEY = null;
private USER_NAME = null; private USER_NAME = null;
private UPDATE_URL = "https://codestats.net/api/"; private UPDATE_URL = "https://codestats.net/api";
private headers: Record<string, string> = {
private axios = null; "Content-Type": "application/json"
}
constructor(apiKey: string, apiURL: string, userName: string) { constructor(apiKey: string, apiURL: string, userName: string) {
this.updateSettings(apiKey, apiURL, userName); this.updateSettings(apiKey, apiURL, userName);
@ -27,19 +38,12 @@ export class CodeStatsAPI {
return; return;
} }
this.axios = axios.default.create({ this.headers["X-API-Token"] = this.API_KEY
baseURL: this.UPDATE_URL,
timeout: 10000,
headers: {
"X-API-Token": this.API_KEY,
"Content-Type": "application/json"
}
});
} }
public sendUpdate(pulse: Pulse): axios.AxiosPromise { public async sendUpdate(pulse: Pulse): Promise<any> {
// If we did not have API key, don't try to update // If we did not have API key, don't try to update
if (this.axios === null) { if (this.API_KEY === null) {
return null; return null;
} }
@ -55,29 +59,28 @@ export class CodeStatsAPI {
let json: string = JSON.stringify(data); let json: string = JSON.stringify(data);
console.log(`JSON: ${json}`); console.log(`JSON: ${json}`);
return this.axios try {
.post("my/pulses", json) const response = await realFetch(`${this.UPDATE_URL}/my/pulses`, {
.then(response => { method: 'POST',
console.log(response); body: json,
headers: this.headers
}) })
.then(() => { console.log(response)
pulse.reset(); pulse.reset()
}) } catch (error) {
.catch(error => { console.log(error)
console.log(error); }
});
} }
public getProfile(): axios.AxiosPromise { public async getProfile(): Promise<any> {
return this.axios try {
.get(`users/${this.USER_NAME}`) const resp = await realFetch(`${this.UPDATE_URL}/users/${this.USER_NAME}`)
.then(response => { const response = await resp.json()
return response.data; return response
}) } catch (error) {
.catch(error => { console.log(error)
console.log(error); return null
return null; }
});
} }
} }

View File

@ -1,6 +1,5 @@
"use strict"; import { ExtensionContext } from "vscode"
import { ExtensionContext } from "vscode"; import { XpCounter } from "./xp-counter"
import { XpCounter } from "./xp-counter";
export function activate(context: ExtensionContext): void { export function activate(context: ExtensionContext): void {
let controller: XpCounter = new XpCounter(context); let controller: XpCounter = new XpCounter(context);

View File

@ -1,8 +1,7 @@
import * as fs from 'fs'; import { CancellationToken, Event, ExtensionContext, TextDocumentContentProvider, Uri, workspace } from "vscode"
import { CancellationToken, Event, ExtensionContext, TextDocumentContentProvider, Uri } from "vscode"; import { CodeStatsAPI } from "./code-stats-api"
import { CodeStatsAPI } from "./code-stats-api"; import profileHtmlEex from './profile.html.eex'
import * as path from 'path';
import template = require('lodash.template'); import template = require('lodash.template');
@ -19,8 +18,7 @@ export class ProfileProvider implements TextDocumentContentProvider {
provideTextDocumentContent(uri: Uri, token: CancellationToken): string | Thenable<string> { provideTextDocumentContent(uri: Uri, token: CancellationToken): string | Thenable<string> {
if( token.isCancellationRequested ) if (token.isCancellationRequested) return;
return;
const LEVEL_FACTOR = 0.025; const LEVEL_FACTOR = 0.025;
@ -47,10 +45,9 @@ export class ProfileProvider implements TextDocumentContentProvider {
} }
function getSortedArray(obj: any): any[] { function getSortedArray(obj: any): any[] {
let items = []; let items = [];
for( let prop in obj) { for (const prop in obj) {
let item = obj[prop]; let item = obj[prop];
let percents = getLevelProgress(item.xps, item.new_xps); let percents = getLevelProgress(item.xps, item.new_xps);
items.push( items.push(
@ -68,14 +65,15 @@ export class ProfileProvider implements TextDocumentContentProvider {
return items.sort( (a,b) => {return b.xp - a.xp;}); return items.sort( (a,b) => {return b.xp - a.xp;});
} }
return this.api.getProfile().then(profile => { return this.api.getProfile().then(async (profile) => {
if( profile === null ) if( profile === null )
{ {
return `<h1>Can't fetch profile. Please try again later</h1> Make sure <strong>codestats.username</strong> setting is set to correct user name.`; return `<h1>Can't fetch profile. Please try again later</h1> Make sure <strong>codestats.username</strong> setting is set to correct user name.`;
} }
let htmlTemplate = fs.readFileSync(this.context.asAbsolutePath("assets/profile.html.eex")); // don't look at this file please
let htmlTemplate = profileHtmlEex
profile["level"] = getLevel(profile["total_xp"]); profile["level"] = getLevel(profile["total_xp"]);
@ -89,10 +87,7 @@ export class ProfileProvider implements TextDocumentContentProvider {
let html = template(htmlTemplate); let html = template(htmlTemplate);
const stylePath = Uri.file(path.join(this.context.extensionPath, 'assets', 'profile.css')); return html({profile: profile, languages: languages, machines: machines});
const styleSrc = stylePath.with({scheme: 'vscode-resource'});
return html({profile: profile, languages: languages, machines: machines, style: styleSrc});
}); });
} }

191
src/profile.html.eex.ts Normal file
View File

@ -0,0 +1,191 @@
export default `
<style>
/* Dark theme color palette */
.vscode-dark {
--color-language: #dbd5b9;
--color-primary: #E3C23D;
--color-secondary: #5D5535;
--color-bar-background: #303030;
--color-bar-border: #424242;
}
/* Light theme color palette */
.vscode-light {
--color-language: #1B2334;
--color-primary: #5D78B3;
--color-secondary: #354567;
--color-bar-background: #939DB3;
--color-bar-border: #697080;
}
.profile {
color: var(--color-language);
}
h3 {
text-align: center;
}
sup {
top: -.5em;
font-size: 75%;
color: var(--color-primary);
}
.language-progress {
float: left;
position: relative;
width: 6rem;
height: 6rem;
}
.language-progress .tooltiptext {
visibility: hidden;
width: 120px;
background-color: var(--color-bar-border);
color: var(--color-language);
text-align: center;
border-radius: 6px;
padding: 5px 0;
position: absolute;
z-index: 1;
}
.language-progress:hover .tooltiptext {
visibility: visible;
}
.language-progress svg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
transform: rotate(-90deg);
}
circle {
stroke-width: 5;
fill:transparent;
}
circle.backg {
stroke: var(--color-bar-background);
}
circle.newxp {
stroke: var(--color-primary);
}
circle.oldxp {
stroke: var(--color-secondary);
}
.language {
padding-top: 2rem;
margin: auto;
text-align: center;
color: var(--color-language);
}
.language span {
display: block;
font-weight: bold;
font-size: 1.1em;
}
.machines {
clear: both;
margin-top: 1rem;
padding-top: 1rem;
}
.machine {
float: none;
}
.progress {
height: 20px;
margin-bottom: 20px;
background-color: var(--color-bar-background);
border-radius: 4px;
border: solid 1px;
border-color: var(--color-bar-border);
}
.progress-bar {
float: left;
width: 0%;
height: 100%;
font-size: 12px;
line-height: 20px;
text-align: center;
background-color: var(--color-secondary);
}
.progress-bar-new {
background-color: var(--color-primary);
}
</style>
<div class="profile">
<h3> \${profile.user} <sup>\${profile.level}</sup> \${profile.total_xp} xp
<% if( profile.new_xp > 0 ) { %>
<sup>+<%= profile.new_xp %></sup>
<% } %>
</h3>
<div class="progress">
<div class="progress-bar" style='width:\${profile.progress}%;'>
</div>
<div class="progress-bar progress-bar-new" style='width:\${profile.new_progress}%;'>
</div>
</div>
<div class="languages">
<% for( let l in languages) { %>
<div class="language-progress">
<span class="tooltiptext">
<strong><%=languages[l].xp %> xp</strong>
<% if( languages[l].new_xp > 0 ) { %>
<sup>+<%= languages[l].new_xp %></sup>
<% } %>
</span>
<svg viewBox="0 0 100 100">
<circle class="backg" cx="50" cy="50" r="45" ></circle>
<circle class="newxp" cx="50" cy="50" r="45" stroke-dasharray="282.6" stroke-dashoffset='\${ ((100-languages[l].progress) * 282.6 / 100) }'></circle>
<circle class="oldxp" cx="50" cy="50" r="45" stroke-dasharray="282.6" stroke-dashoffset='\${ ((100-languages[l].progress + languages[l].new_progress) * 282.6 / 100) }'></circle>
</svg>
<div class="language">
<%= languages[l].name %>
<span>
<%= languages[l].level %>
</span>
</div>
</div>
<% } %>
</div>
<p/>
<div class="machines">
<% for( let m in machines) { %>
<div class="machine">
<strong>
<%= machines[m].name %>
</strong>
<sup> <%= machines[m].level %> </sup>
<%= machines[m].xp %> xp
<% if( machines[m].new_xp > 0 ) { %>
<sup>+<%= machines[m].new_xp %></sup>
<% } %>
<div class="progress">
<div class="progress-bar" style='width:\${machines[m].progress}%;'>
</div>
<div class="progress-bar progress-bar-new" style='width:\${machines[m].new_progress}%;'>
</div>
</div>
</div>
<% } %>
</div>
<div>`

View File

@ -1,22 +1,12 @@
import * as path from 'path'
import { import {
Disposable, commands, Disposable, ExtensionContext, StatusBarAlignment, StatusBarItem,
workspace, TextDocument, TextDocumentChangeEvent, Uri,
window, ViewColumn, window, workspace, WorkspaceConfiguration
Uri, } from "vscode"
ViewColumn, import { CodeStatsAPI } from "./code-stats-api"
commands, import { ProfileProvider } from "./profile-provider"
StatusBarItem, import { Pulse } from "./pulse"
TextDocument,
StatusBarAlignment,
TextDocumentChangeEvent,
WorkspaceConfiguration,
ExtensionContext
} from "vscode";
import { Pulse } from "./pulse";
import { CodeStatsAPI } from "./code-stats-api";
import { ProfileProvider } from "./profile-provider";
import * as path from 'path';
export class XpCounter { export class XpCounter {
private combinedDisposable: Disposable; private combinedDisposable: Disposable;
private statusBarItem: StatusBarItem; private statusBarItem: StatusBarItem;