diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6be39383..997590a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,16 @@ Seja Bem-Vindo a nossa página e atualização da Extensão da Discloud. Aqui vo
---
+## 2.29.7
+
+- Adição de ícones na interface da extensão, presentes em praticamente todas as seções, incluindo Apps (ao lado de Bot e Site), User.
+- Correção de bugs no sistema de tradução, que anteriormente não aplicava grande parte dos textos.
+- Melhoria no gerenciamento de subdomínios, permitindo criar e deletar diretamente pela extensão, sem precisar acessar o dashboard.
+- Atualizações na interface de User:
+ - Exibição da RAM em MB de forma mais organizada
+ - Visualização do plano com suporte a português/inglês, acompanhado de ícone
+ - Identificação do idioma mais clara (ex: de pt-BR para Português (BR))
+
## 2.29.6
- Correção no sistema de autenticação, e orquestração de sessão.
diff --git a/discloudconfigschema.json b/discloudconfigschema.json
index 8eb9538b..0871f744 100644
--- a/discloudconfigschema.json
+++ b/discloudconfigschema.json
@@ -12,8 +12,11 @@
"ffmpeg",
"java",
"libgl",
+ "mysql",
"openssl",
"puppeteer",
+ "selenium",
+ "tesseract",
"tools"
]
}
diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json
index 83721d69..3fa7a301 100644
--- a/l10n/bundle.l10n.json
+++ b/l10n/bundle.l10n.json
@@ -2,7 +2,7 @@
"action.cancel": "Cancel",
"action.cancelled": "Action cancelled",
"action.ok": "Yes",
- "action.title": "Are you sure that you want to do {action}?",
+ "action.title": "Are you sure you want to perform this action?",
"app.status.errorCode": "Code error",
"app.status.off": "Off",
"app.status.on": "On",
@@ -60,10 +60,16 @@
"input.set.locale.available.title": "These are the available languages.",
"input.set.locale.prompt": "Set your language.",
"input.set.locale.validate": "Language must be like en-US.",
+ "input.subdomain.prompt": "Enter the subdomain name.",
+ "input.subdomain.validate": "Subdomain must use 2-20 lowercase letters, numbers or hyphens.",
"invalid.discloud.config": "The discloud.config file is invalid or does not exist.",
"invalid.discloud.config.main": "The {file} file specified in the main scope does not exist.",
"invalid.input": "Invalid input",
"invalid.token": "Invalid token.",
+ "label.user": "User",
+ "label.customdomain": "Custom Domains",
+ "label.teamapps": "Team Apps",
+ "label.subdomain": "Subdomains",
"label.apps.amount": "Amount of Apps",
"label.available.ram": "Available RAM",
"label.cpu": "CPU",
@@ -81,6 +87,7 @@
"missing.discloud.config.main": "The {file} file was not found in the list to ZIP.",
"missing.input": "Missing input",
"missing.locale": "Missing locale",
+ "missing.subdomain": "Missing subdomain",
"missing.moderator": "Missing mod",
"missing.moderator.id": "Missing mod ID",
"missing.team.appid": "Team app ID not found.",
@@ -126,6 +133,8 @@
"progress.start.title": "Start",
"progress.status.title": "Status",
"progress.stop.title": "Stop",
+ "progress.subdomain.create.title": "Create subdomain",
+ "progress.subdomain.delete.title": "Delete subdomain",
"progress.upload.title": "Upload",
"ratelimited": "You have reached the request limit. Try again in {s} seconds.",
"readdiscloudconfigdocs": "Read the [documentation](https://docs.discloud.com/en/discloud.config).",
@@ -170,4 +179,4 @@
"valid.token": "Token set up successfully!",
"your.app.is.now": "Your app {app} is now",
"your.team.app.is.now": "Your team app {app} is now"
-}
\ No newline at end of file
+}
diff --git a/l10n/bundle.l10n.pt.json b/l10n/bundle.l10n.pt.json
index 9ffe0162..217bc013 100644
--- a/l10n/bundle.l10n.pt.json
+++ b/l10n/bundle.l10n.pt.json
@@ -2,7 +2,7 @@
"action.cancel": "Cancelar",
"action.cancelled": "Ação cancelada",
"action.ok": "Ok",
- "action.title": "Tem certeza que quer fazer {action}?",
+ "action.title": "Tem certeza de que deseja realizar esta ação?",
"app.status.errorCode": "Erro de código",
"app.status.off": "Desligado",
"app.status.on": "Ligado",
@@ -60,10 +60,19 @@
"input.set.locale.available.title": "Estes são os idiomas disponíveis.",
"input.set.locale.prompt": "Defina seu idioma.",
"input.set.locale.validate": "Idioma deve ser como pt-BR.",
+ "input.subdomain.prompt": "Digite o nome do subdominio.",
+ "input.subdomain.validate": "O subdominio deve usar 2-20 letras minusculas, numeros ou hifens.",
+ "missing.subdomain": "Subdominio nao encontrado",
+ "progress.subdomain.create.title": "Criar subdominio",
+ "progress.subdomain.delete.title": "Remover subdominio",
"invalid.discloud.config": "O arquivo discloud.config é inválido ou não existe.",
"invalid.discloud.config.main": "O arquivo {file} especificado no escopo principal não existe.",
"invalid.input": "Entrada inválida",
"invalid.token": "Token inválido.",
+ "label.user": "Usuário",
+ "label.customdomain": "Domínios Customizados",
+ "label.teamapps": "Apps de Equipe",
+ "label.subdomain": "Subdomínios",
"label.apps.amount": "Quant. de apps",
"label.available.ram": "RAM disponível",
"label.cpu": "CPU",
@@ -170,4 +179,4 @@
"valid.token": "Token configurado com sucesso!",
"your.app.is.now": "Seu app {app} agora está",
"your.team.app.is.now": "Seu app de equipe {app} agora está"
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index 33610ebe..aab4de8c 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Discloud",
"description": "Somos uma plataforma de nuvem baseada em contêiner que nasceu com a vontade de oferecer hospedagem fácil, rápida, gratuita e de qualidade.",
"publisher": "discloud",
- "version": "2.29.6",
+ "version": "2.29.7",
"engines": {
"vscode": "^1.118.0"
},
@@ -25,7 +25,13 @@
"discloud.app",
"discloudbot",
"discord",
- "host"
+ "host",
+ "hospedagem",
+ "host free",
+ "discloud.com",
+ "discloudbot.com",
+ "hosting",
+ "discord"
],
"categories": [
"Other"
@@ -244,6 +250,16 @@
"title": "%command.subdomain.refresh%",
"icon": "$(refresh)"
},
+ {
+ "command": "discloud.subdomain.create",
+ "title": "%command.subdomain.create%",
+ "icon": "$(add)"
+ },
+ {
+ "command": "discloud.subdomain.delete",
+ "title": "%command.subdomain.delete%",
+ "icon": "$(trash)"
+ },
{
"command": "discloud.team.backup",
"title": "%command.team.backup%",
@@ -1005,6 +1021,11 @@
"command": "discloud.user.set.locale",
"when": "view == discloudUser && discloudAuthorized"
},
+ {
+ "command": "discloud.subdomain.delete",
+ "group": "inline",
+ "when": "view == discloudSubdomains && viewItem =~ /^SubDomainTreeItem/"
+ },
{
"command": "discloud.logout",
"group": "inline",
@@ -1048,6 +1069,11 @@
"when": "view == discloudSubdomains",
"group": "navigation"
},
+ {
+ "command": "discloud.subdomain.create",
+ "when": "view == discloudSubdomains",
+ "group": "navigation"
+ },
{
"command": "discloud.team.refresh",
"when": "view == discloudTeamApps",
@@ -1237,7 +1263,7 @@
"icon": "resources/icons/discloud_icon.svg",
"name": "%view.subdomain.title%",
"visibility": "collapsed",
- "when": "discloudAuthorized"
+ "when": "discloudAuthorized && discloudHasSubdomainsAccess"
},
{
"type": "tree",
@@ -1245,7 +1271,7 @@
"icon": "resources/icons/discloud_icon.svg",
"name": "%view.domain.title%",
"visibility": "collapsed",
- "when": "discloudAuthorized"
+ "when": "discloudAuthorized && discloudHasCustomDomainsAccess"
},
{
"type": "tree",
@@ -1325,6 +1351,7 @@
"bytes": "^3.1.2",
"jose": "^6.2.3",
"json-schema-library": "^11.4.1",
+ "path": "^0.12.7",
"ws": "^8.21.0",
"yocto-queue": "^1.2.2"
},
diff --git a/package.nls.json b/package.nls.json
index 1c7606e1..c354dd17 100644
--- a/package.nls.json
+++ b/package.nls.json
@@ -25,6 +25,8 @@
"command.login": "Login Discloud",
"command.logout": "Logout Discloud",
"command.logs": "Get app logs",
+ "command.subdomain.create": "Create subdomain",
+ "command.subdomain.delete": "Delete subdomain",
"command.subdomain.refresh": "Refresh subdomain list",
"command.team.backup": "Backup team app",
"command.team.commit": "Commit team app",
@@ -76,7 +78,7 @@
"config.team.sort.by.title": "Sort team app by",
"config.team.sort.online.description": "Show team apps with online status first",
"config.team.sort.online.title": "Show online team app first",
- "config.token.description": "Your [Discloud](https://discloudbot.com) API token.",
+ "config.token.description": "Your [Discloud](https://discloud.com) API token.",
"config.upload.focus.logs": "Focus of logs during upload",
"config.upload.focus.logs.always.label": "Always",
"config.upload.focus.logs.errors.label": "Errors",
@@ -110,4 +112,4 @@
"welcome": "Welcome!",
"welcome.token.missing": "You have not yet provided a token to access Discloud resources.\n[Submit your Discloud token](command:discloud.login)",
"welcome.token.unauthorized": "You provided an invalid token to access Discloud resources.\n[Submit your Discloud token](command:discloud.login)"
-}
\ No newline at end of file
+}
diff --git a/package.nls.pt.json b/package.nls.pt.json
index ad6af558..7ab42076 100644
--- a/package.nls.pt.json
+++ b/package.nls.pt.json
@@ -25,6 +25,8 @@
"command.login": "Login Discloud",
"command.logout": "Logout Discloud",
"command.logs": "Obter logs do app",
+ "command.subdomain.create": "Criar subdominio",
+ "command.subdomain.delete": "Remover subdominio",
"command.subdomain.refresh": "Atualizar lista de subdomínios",
"command.team.backup": "Backup app de equipe",
"command.team.commit": "Commitar app de equipe",
@@ -76,7 +78,7 @@
"config.team.sort.by.title": "Ordenar app de equipe por",
"config.team.sort.online.description": "Mostrar apps de equipe com status online primeiro.",
"config.team.sort.online.title": "Mostrar apps de equipe online primeiro",
- "config.token.description": "Seu token da API da [Discloud](https://discloudbot.com).",
+ "config.token.description": "Seu token da API da [Discloud](https://discloud.com).",
"config.upload.focus.logs": "Foco dos logs durante o upload",
"config.upload.focus.logs.always.label": "Sempre",
"config.upload.focus.logs.errors.label": "Erros",
@@ -110,4 +112,4 @@
"welcome": "Bem vindo!",
"welcome.token.missing": "Você ainda não forneceu um token para ter acesso aos recursos da Discloud.\n[Envie seu token Discloud](command:discloud.login)",
"welcome.token.unauthorized": "Você forneceu um token inválido para acessar os recursos do Discloud.\n[Envie seu token Discloud](command:discloud.login)"
-}
\ No newline at end of file
+}
diff --git a/resources/dark/bot.svg b/resources/dark/bot.svg
new file mode 100644
index 00000000..dd88ace2
--- /dev/null
+++ b/resources/dark/bot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/icons/carbon.png b/resources/icons/carbon.png
new file mode 100644
index 00000000..98c9769c
Binary files /dev/null and b/resources/icons/carbon.png differ
diff --git a/resources/icons/diamond.png b/resources/icons/diamond.png
new file mode 100644
index 00000000..216321c3
Binary files /dev/null and b/resources/icons/diamond.png differ
diff --git a/resources/icons/free.png b/resources/icons/free.png
new file mode 100644
index 00000000..49ac431f
Binary files /dev/null and b/resources/icons/free.png differ
diff --git a/resources/icons/gold.png b/resources/icons/gold.png
new file mode 100644
index 00000000..4cecae69
Binary files /dev/null and b/resources/icons/gold.png differ
diff --git a/resources/icons/krypton.png b/resources/icons/krypton.png
new file mode 100644
index 00000000..4add6110
Binary files /dev/null and b/resources/icons/krypton.png differ
diff --git a/resources/icons/platinum.png b/resources/icons/platinum.png
new file mode 100644
index 00000000..8153ebc4
Binary files /dev/null and b/resources/icons/platinum.png differ
diff --git a/resources/icons/ruby.png b/resources/icons/ruby.png
new file mode 100644
index 00000000..c6652745
Binary files /dev/null and b/resources/icons/ruby.png differ
diff --git a/resources/icons/sapphire.png b/resources/icons/sapphire.png
new file mode 100644
index 00000000..8fba5aeb
Binary files /dev/null and b/resources/icons/sapphire.png differ
diff --git a/resources/icons/vibranium.png b/resources/icons/vibranium.png
new file mode 100644
index 00000000..17ad374a
Binary files /dev/null and b/resources/icons/vibranium.png differ
diff --git a/resources/light/bot.svg b/resources/light/bot.svg
new file mode 100644
index 00000000..812ca349
--- /dev/null
+++ b/resources/light/bot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/@types/api.ts b/src/@types/api.ts
index 68d71339..dfa2ed1d 100644
--- a/src/@types/api.ts
+++ b/src/@types/api.ts
@@ -13,6 +13,7 @@ export interface ApiVscodeUser {
customdomains: string[]
locale: string
plan: string
+ planDataEnd?: string | null
ramUsedMb: number
subdomains: string[]
totalRamMb: number
@@ -35,3 +36,20 @@ export interface ApiVscodeApp extends BaseApiApp {
type: AppType
version: string
}
+
+export interface ApiSubdomain {
+ date: number
+ id: string
+ status: number
+ userID: string
+}
+
+export interface RESTGetApiSubdomainResult extends RESTApiBaseResult {
+ subdomain?: ApiSubdomain
+}
+
+export interface RESTPostApiSubdomainResult extends RESTApiBaseResult {
+ subdomain?: ApiSubdomain
+}
+
+export interface RESTDeleteApiSubdomainResult extends RESTApiBaseResult {}
\ No newline at end of file
diff --git a/src/commands.ts b/src/commands.ts
index 1f256862..e5ef08b5 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -62,8 +62,10 @@ export async function commandsRegister(core: ExtensionCore) {
commandModuleRegister(core, "domain/refresh", await import("./commands/domain/refresh"));
// subdomain
+ commandModuleRegister(core, "subdomain/create", await import("./commands/subdomain/create"));
+ commandModuleRegister(core, "subdomain/delete", await import("./commands/subdomain/delete"));
commandModuleRegister(core, "subdomain/refresh", await import("./commands/subdomain/refresh"));
-
+
// team
commandModuleRegister(core, "team/backup", await import("./commands/team/backup"));
commandModuleRegister(core, "team/commit", await import("./commands/team/commit"));
diff --git a/src/commands/subdomain/create.ts b/src/commands/subdomain/create.ts
new file mode 100644
index 00000000..9bbdff6b
--- /dev/null
+++ b/src/commands/subdomain/create.ts
@@ -0,0 +1,39 @@
+import { t } from "@vscode/l10n";
+import { ProgressLocation, window } from "vscode";
+import { type RESTPostApiSubdomainResult, type TaskData } from "../../@types";
+import type ExtensionCore from "../../core/extension";
+import Command from "../../structures/Command";
+
+const SUBDOMAIN_NAME_REGEXP = /^[a-z0-9-]{2,20}$/;
+
+export default class extends Command {
+ constructor(core: ExtensionCore) {
+ super(core, {
+ progress: {
+ location: ProgressLocation.Notification,
+ title: t("progress.subdomain.create.title"),
+ },
+ });
+ }
+
+ async run(_: TaskData, subdomainName?: string) {
+ if (typeof subdomainName !== "string" || !SUBDOMAIN_NAME_REGEXP.test(subdomainName)) {
+ subdomainName = await window.showInputBox({
+ prompt: t("input.subdomain.prompt"),
+ validateInput(value) {
+ if (!SUBDOMAIN_NAME_REGEXP.test(value))
+ return t("input.subdomain.validate");
+ },
+ });
+ }
+
+ if (!subdomainName) throw Error(t("missing.subdomain"));
+
+ const response = await this.core.api.post(`/subdomain/${subdomainName}`);
+ if (!response) return;
+
+ void window.showInformationMessage(response.message ?? t("done"));
+
+ await this.core.user.fetch(true);
+ }
+}
diff --git a/src/commands/subdomain/delete.ts b/src/commands/subdomain/delete.ts
new file mode 100644
index 00000000..51b67b66
--- /dev/null
+++ b/src/commands/subdomain/delete.ts
@@ -0,0 +1,35 @@
+import { t } from "@vscode/l10n";
+import { ProgressLocation, window } from "vscode";
+import { type RESTDeleteApiSubdomainResult, type TaskData } from "../../@types";
+import type ExtensionCore from "../../core/extension";
+import Command from "../../structures/Command";
+import type SubDomainTreeItem from "../../structures/SubDomainTreeItem";
+
+export default class extends Command {
+ constructor(core: ExtensionCore) {
+ super(core, {
+ progress: {
+ location: ProgressLocation.Notification,
+ title: t("progress.subdomain.delete.title"),
+ },
+ });
+ }
+
+ async run(_: TaskData, item: SubDomainTreeItem) {
+ const subdomainName = item.subdomain;
+
+ await this.confirmAction({
+ action: subdomainName,
+ throwOnReject: true,
+ title: "action.title",
+ type: "showWarningMessage",
+ });
+
+ const response = await this.core.api.delete(`/subdomain/${subdomainName}`);
+ if (!response) return;
+
+ void window.showInformationMessage(response.message ?? t("done"));
+
+ await this.core.user.fetch(true);
+ }
+}
diff --git a/src/events/activate.ts b/src/events/activate.ts
index f3a10eea..63f1805b 100644
--- a/src/events/activate.ts
+++ b/src/events/activate.ts
@@ -52,6 +52,10 @@ core.on("activate", async function (context) {
core.logger.debug("Activate: done");
+ await commands.executeCommand("setContext", "discloudInitialized", true);
+ await commands.executeCommand("setContext", "discloudHasSubdomainsAccess", false);
+ await commands.executeCommand("setContext", "discloudHasCustomDomainsAccess", false);
+
await migrateAuthenticationProvider(core);
const session = await core.auth.getSession();
@@ -61,8 +65,6 @@ core.on("activate", async function (context) {
} else {
core.statusBar.reset();
}
-
- await commands.executeCommand("setContext", "discloudInitialized", true);
});
async function migrateAuthenticationProvider(core: ExtensionCore) {
diff --git a/src/events/missingToken.ts b/src/events/missingToken.ts
index 933cfc37..61eba2a5 100644
--- a/src/events/missingToken.ts
+++ b/src/events/missingToken.ts
@@ -1,16 +1,23 @@
import { t } from "@vscode/l10n";
import { commands } from "vscode";
import core from "../extension";
+import { localize } from "../localize";
core.on("missingToken", async function () {
await Promise.all([
commands.executeCommand("setContext", "discloudAuthorized", false),
commands.executeCommand("setContext", "discloudUnauthorized", false),
+ commands.executeCommand("setContext", "discloudHasSubdomainsAccess", false),
+ commands.executeCommand("setContext", "discloudHasCustomDomainsAccess", false),
+ localize(core.context),
]);
core.api.authorized = false;
core.userTree.clear();
+
+ core.subDomainTree.update([]);
+ core.customDomainTree.update([]);
core.statusBar.setLogin();
core.logger.warn(t("missing.token"));
diff --git a/src/events/unauthorized.ts b/src/events/unauthorized.ts
index 52101eee..d8f8252a 100644
--- a/src/events/unauthorized.ts
+++ b/src/events/unauthorized.ts
@@ -1,15 +1,21 @@
import { commands } from "vscode";
import core from "../extension";
+import { localize } from "../localize";
core.on("unauthorized", async function () {
await Promise.all([
commands.executeCommand("setContext", "discloudAuthorized", false),
commands.executeCommand("setContext", "discloudUnauthorized", true),
+ commands.executeCommand("setContext", "discloudHasSubdomainsAccess", false),
+ commands.executeCommand("setContext", "discloudHasCustomDomainsAccess", false),
+ localize(core.context),
]);
core.api.authorized = false;
core.userTree.clear();
+ core.subDomainTree.update([]);
+ core.customDomainTree.update([]);
core.statusBar.setLogin();
diff --git a/src/events/vscode.ts b/src/events/vscode.ts
index 6b75f848..8b4f91b6 100644
--- a/src/events/vscode.ts
+++ b/src/events/vscode.ts
@@ -1,8 +1,20 @@
+import { commands } from "vscode";
import core from "../extension";
+import { localize } from "../localize";
+import { canAccessCustomDomains, canAccessSubdomains } from "../utils/plans";
core.on("vscode", async function (user) {
if (!user) return;
+ const hasSubdomainsAccess = canAccessSubdomains(user.plan);
+ const hasCustomDomainsAccess = canAccessCustomDomains(user.plan);
+
+ await Promise.all([
+ commands.executeCommand("setContext", "discloudHasSubdomainsAccess", hasSubdomainsAccess),
+ commands.executeCommand("setContext", "discloudHasCustomDomainsAccess", hasCustomDomainsAccess),
+ localize(core.context, user.locale || undefined),
+ ]);
+
core.userTree.set(user);
if ("appsStatus" in user)
diff --git a/src/localize.ts b/src/localize.ts
index 38a24add..94342230 100644
--- a/src/localize.ts
+++ b/src/localize.ts
@@ -3,6 +3,8 @@ import { open } from "fs/promises";
import { join } from "path";
import { env, type ExtensionContext } from "vscode";
+const localizationContentsCache = new Map();
+
async function importJSON(path: string): Promise {
try {
const fileHandle = await open(path);
@@ -14,19 +16,42 @@ async function importJSON(path: string): Promise {
return {};
}
-export async function localize(context: ExtensionContext) {
- const firstLanguagePart = env.language.split(/\W+/)[0];
+function getLocaleCandidates(locale = env.language) {
+ const normalizedLocale = locale.trim();
+ const lowerCasedLocale = normalizedLocale.toLowerCase();
+ const firstLanguagePart = lowerCasedLocale.split(/\W+/)[0];
+
+ return {
+ cacheKey: lowerCasedLocale || "default",
+ locale: lowerCasedLocale,
+ firstLanguagePart,
+ };
+}
+
+async function getLocalizationContents(context: ExtensionContext, locale = env.language) {
+ const { cacheKey, locale: normalizedLocale, firstLanguagePart } = getLocaleCandidates(locale);
+ const cached = localizationContentsCache.get(cacheKey);
+
+ if (cached) return cached;
+
const bundleDir: string = context.extension.packageJSON.l10n;
+ const contents = Object.assign({}, ...await Promise.all([
+ importJSON(context.asAbsolutePath("package.nls.json")),
+ importJSON(context.asAbsolutePath(`package.nls.${firstLanguagePart}.json`)),
+ importJSON(context.asAbsolutePath(`package.nls.${normalizedLocale}.json`)),
+ ].concat(bundleDir ? [
+ importJSON(context.asAbsolutePath(join(bundleDir, "bundle.l10n.json"))),
+ importJSON(context.asAbsolutePath(join(bundleDir, `bundle.l10n.${firstLanguagePart}.json`))),
+ importJSON(context.asAbsolutePath(join(bundleDir, `bundle.l10n.${normalizedLocale}.json`))),
+ ] : [])));
+
+ localizationContentsCache.set(cacheKey, contents);
+
+ return contents;
+}
+export async function localize(context: ExtensionContext, locale = env.language) {
config({
- contents: Object.assign({}, ...await Promise.all([
- importJSON(context.asAbsolutePath("package.nls.json")),
- importJSON(context.asAbsolutePath(`package.nls.${firstLanguagePart}.json`)),
- importJSON(context.asAbsolutePath(`package.nls.${env.language}.json`)),
- ].concat(bundleDir ? [
- importJSON(context.asAbsolutePath(join(bundleDir, "bundle.l10n.json"))),
- importJSON(context.asAbsolutePath(join(bundleDir, `bundle.l10n.${firstLanguagePart}.json`))),
- importJSON(context.asAbsolutePath(join(bundleDir, `bundle.l10n.${env.language}.json`))),
- ] : []))),
+ contents: await getLocalizationContents(context, locale),
});
}
diff --git a/src/structures/BaseChildTreeItem.ts b/src/structures/BaseChildTreeItem.ts
index c37ce5af..ca2d83bc 100644
--- a/src/structures/BaseChildTreeItem.ts
+++ b/src/structures/BaseChildTreeItem.ts
@@ -2,7 +2,7 @@ import { type Disposable, TreeItem, type TreeItemCollapsibleState, type TreeItem
import { type BaseChildTreeItemData } from "../@types";
export default abstract class BaseChildTreeItem extends TreeItem implements Disposable {
- readonly contextKey = "ChildTreeItem";
+ readonly contextKey: string = "ChildTreeItem";
contextValue = this.contextKey;
constructor(label: string | TreeItemLabel, collapsibleState?: TreeItemCollapsibleState) {
diff --git a/src/structures/SubDomainTreeItem.ts b/src/structures/SubDomainTreeItem.ts
index 143b3ae9..6c80e5ab 100644
--- a/src/structures/SubDomainTreeItem.ts
+++ b/src/structures/SubDomainTreeItem.ts
@@ -6,6 +6,7 @@ import { getIconName, getIconPath } from "../utils/utils";
import BaseTreeItem from "./BaseTreeItem";
export default class SubDomainTreeItem extends BaseTreeItem {
+ readonly contextKey = "SubDomainTreeItem";
declare subdomain: string;
declare iconName: string;
@@ -31,6 +32,7 @@ export default class SubDomainTreeItem extends BaseTreeItem {
this.iconPath = getIconPath(this.iconName);
this.tooltip = t(`app.status.${this.iconName}`) + " - " + this.label;
+ this.contextValue = this.contextKey;
this.collapsibleState =
this.children.size ?
diff --git a/src/structures/UserAppTreeItem.ts b/src/structures/UserAppTreeItem.ts
index 40dd0ce6..a620d7c9 100644
--- a/src/structures/UserAppTreeItem.ts
+++ b/src/structures/UserAppTreeItem.ts
@@ -1,7 +1,7 @@
import { type ApiStatusApp } from "@discloudapp/api-types/v2";
import { calculatePercentage } from "@discloudapp/util";
import { t } from "@vscode/l10n";
-import { type LogOutputChannel, TreeItemCollapsibleState, Uri } from "vscode";
+import { type LogOutputChannel, ThemeIcon, TreeItemCollapsibleState, Uri } from "vscode";
import { AppType } from "../@enum";
import { type ApiVscodeApp, type UserAppChildTreeItemData, type UserAppTreeItemData } from "../@types";
import core from "../extension";
@@ -104,6 +104,7 @@ export default class UserAppTreeItem extends BaseTreeItem
label: data.clusterName,
description: t("cluster"),
iconName: "container",
+ iconPath: new ThemeIcon("server"),
});
if (data.memory)
diff --git a/src/structures/UserAppTypeTreeItemView.ts b/src/structures/UserAppTypeTreeItemView.ts
index b2be9239..25f9dd15 100644
--- a/src/structures/UserAppTypeTreeItemView.ts
+++ b/src/structures/UserAppTypeTreeItemView.ts
@@ -1,17 +1,31 @@
import { t } from "@vscode/l10n";
-import { TreeItemCollapsibleState } from "vscode";
+import { ThemeIcon, TreeItemCollapsibleState, Uri } from "vscode";
import { AppType } from "../@enum";
import BaseTreeItem from "./BaseTreeItem";
import type UserAppTreeItem from "./UserAppTreeItem";
+import path from "path";
export default class AppTypeTreeItemView extends BaseTreeItem {
constructor(readonly type: AppType) {
super(t(AppType[type]), TreeItemCollapsibleState.Expanded);
this.contextValue = this.contextKey;
+ this.iconPath = this.getTypeIcon();
+ this.refresh();
}
readonly contextKey = "TreeView";
+ private getTypeIcon() {
+ if (this.type === AppType.site) {
+ return new ThemeIcon("globe");
+ }
+
+ return {
+ light: Uri.file(path.join(__dirname, "../resources/light/bot.svg")),
+ dark: Uri.file(path.join(__dirname, "../resources/dark/bot.svg")),
+ };
+ }
+
dispose(): void;
dispose(key: string): boolean;
dispose(key?: string) {
@@ -24,7 +38,9 @@ export default class AppTypeTreeItemView extends BaseTreeItem {
}
refresh() {
- this.label = `${t(AppType[this.type])} (${this.children.size})`;
+ this.label = t(AppType[this.type]);
+ this.description = `${this.children.size}`;
+ this.iconPath = this.getTypeIcon();
}
set(key: string, app: UserAppTreeItem) {
diff --git a/src/structures/UserTreeItem.ts b/src/structures/UserTreeItem.ts
index 23f1f988..eca7769b 100644
--- a/src/structures/UserTreeItem.ts
+++ b/src/structures/UserTreeItem.ts
@@ -1,9 +1,55 @@
import { t } from "@vscode/l10n";
-import { TreeItemCollapsibleState, Uri } from "vscode";
+import { ThemeIcon, TreeItemCollapsibleState, Uri } from "vscode";
import { type ApiVscodeUser, type UserTreeItemData } from "../@types";
+import { canAccessCustomDomains, canAccessSubdomains, formatPlanLabel, getPlanIconPath } from "../utils/plans";
+import { getIconPath, getThemedResourceIconPath } from "../utils/utils";
import BaseTreeItem from "./BaseTreeItem";
import UserChildTreeItem from "./UserChildTreeItem";
+function getUserFallbackIconPath() {
+ return getThemedResourceIconPath(
+ "resources/icons/discloud_icon.svg",
+ "resources/icons/discloud_icon.svg",
+ );
+}
+
+function formatRamValue(value: number, locale?: string) {
+ return `${new Intl.NumberFormat(locale || "pt-BR").format(value)} MB`;
+}
+
+function capitalizeFirstLetter(value: string) {
+ return value ? value.charAt(0).toUpperCase() + value.slice(1) : value;
+}
+
+function formatLocaleLabel(locale: string) {
+ const normalizedLocale = locale.replace("_", "-");
+ const [languageCode = normalizedLocale, regionCode] = normalizedLocale.split("-");
+
+ try {
+ const languageLabel = new Intl.DisplayNames([normalizedLocale], { type: "language" }).of(languageCode);
+
+ if (!languageLabel) return locale;
+
+ return regionCode
+ ? `${capitalizeFirstLetter(languageLabel)} ${regionCode.toUpperCase()}`
+ : capitalizeFirstLetter(languageLabel);
+ } catch {
+ return locale;
+ }
+}
+
+function getUserDetailsIcons() {
+ return {
+ apps: getIconPath("container"),
+ domains: new ThemeIcon("globe"),
+ locale: new ThemeIcon("globe"),
+ planDataEnd: new ThemeIcon("calendar"),
+ ram: getIconPath("ram"),
+ subdomains: new ThemeIcon("link"),
+ team: new ThemeIcon("organization"),
+ };
+}
+
export default class UserTreeItem extends BaseTreeItem {
iconName?: string;
readonly userID: string;
@@ -21,6 +67,10 @@ export default class UserTreeItem extends BaseTreeItem {
protected _patch(data: Partial): this {
if (!data) return this;
+ const userDetailsIcons = getUserDetailsIcons();
+
+ this.iconPath = getUserFallbackIconPath();
+
if (data.avatar)
try { this.iconPath = Uri.parse(data.avatar); } catch { }
@@ -37,15 +87,17 @@ export default class UserTreeItem extends BaseTreeItem {
if (typeof data.ramUsedMb === "number" && typeof data.totalRamMb === "number")
this._addChild("ram", {
- label: `${data.ramUsedMb}/${data.totalRamMb}`,
+ label: `${formatRamValue(data.ramUsedMb, data.locale)}/${formatRamValue(data.totalRamMb, data.locale)}`,
description: t("label.available.ram"),
+ iconPath: userDetailsIcons.ram,
userID: this.userID,
});
if (data.plan)
this._addChild("plan", {
- label: data.plan,
+ label: formatPlanLabel(data.plan),
description: t("plan"),
+ iconPath: getPlanIconPath(data.plan),
userID: this.userID,
});
@@ -55,13 +107,15 @@ export default class UserTreeItem extends BaseTreeItem {
new Date(data.planDataEnd).toLocaleDateString() :
data.planDataEnd,
description: t("label.plan.expiration"),
+ iconPath: userDetailsIcons.planDataEnd,
userID: this.userID,
});
if (data.locale)
this._addChild("locale", {
- label: data.locale,
+ label: formatLocaleLabel(data.locale),
description: t("locale"),
+ iconPath: userDetailsIcons.locale,
userID: this.userID,
});
@@ -69,6 +123,7 @@ export default class UserTreeItem extends BaseTreeItem {
this._addChild("apps", {
label: `${data.apps.length}`,
description: t("label.apps.amount"),
+ iconPath: userDetailsIcons.apps,
userID: this.userID,
});
@@ -76,22 +131,29 @@ export default class UserTreeItem extends BaseTreeItem {
this._addChild("team", {
label: `${data.appsTeam.length}`,
description: t("label.team.apps.amount"),
+ iconPath: userDetailsIcons.team,
userID: this.userID,
});
- if (data.customdomains)
+ if (data.plan && data.customdomains && canAccessCustomDomains(data.plan))
this._addChild("domains", {
label: `${data.customdomains.length}`,
description: t("label.domains.amount"),
+ iconPath: userDetailsIcons.domains,
userID: this.userID,
});
+ else
+ this.children.delete("domains");
- if (data.subdomains)
+ if (data.plan && data.subdomains && canAccessSubdomains(data.plan))
this._addChild("subdomains", {
label: `${data.subdomains.length}`,
description: t("label.subdomains.amount"),
+ iconPath: userDetailsIcons.subdomains,
userID: this.userID,
});
+ else
+ this.children.delete("subdomains");
this.collapsibleState =
this.children.size ?
diff --git a/src/structures/VSUser.ts b/src/structures/VSUser.ts
index dc5b29fc..372c773b 100644
--- a/src/structures/VSUser.ts
+++ b/src/structures/VSUser.ts
@@ -12,7 +12,7 @@ export default class VSUser implements ApiVscodeUser {
declare avatar: string | null;
declare locale: string;
declare readonly plan: string;
- declare readonly planDataEnd: string;
+ declare readonly planDataEnd: string | null | undefined;
declare readonly ramUsedMb: number;
declare readonly totalRamMb: number;
declare readonly userID: string;
@@ -67,8 +67,11 @@ export default class VSUser implements ApiVscodeUser {
const response = await core.api.put(Routes.locale(locale));
if (!response) return null;
- if ("locale" in response)
+ if ("locale" in response) {
this.locale = response.locale;
+ await core.globalStorage.update("user", this);
+ core.emit("vscode", this);
+ }
return "body" in response ?
response.body :
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 685baaa3..d44940ed 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -57,6 +57,7 @@ const CONFIG_KEYS = Object.freeze([
const TREE_VIEW_IDS = Object.freeze([
"discloudUserApps",
"discloudTeamApps",
+ "discloudSnapshots",
"discloudSubdomains",
"discloudDomains",
"discloudUser",
diff --git a/src/utils/plans.ts b/src/utils/plans.ts
new file mode 100644
index 00000000..b960a45e
--- /dev/null
+++ b/src/utils/plans.ts
@@ -0,0 +1,75 @@
+import { existsSync } from "fs";
+import { join } from "path";
+import { type TreeItem, Uri } from "vscode";
+import core from "../extension";
+import { RESOURCES_DIR } from "./constants";
+
+interface PlanDefinition {
+ aliases: string[]
+ iconName: string
+ label: string
+ level: number
+}
+
+const PLAN_DEFINITIONS: PlanDefinition[] = [
+ { aliases: ["free", "gratis", "gratuito"], iconName: "free", label: "Free", level: 0 },
+ { aliases: ["carbon", "carbono"], iconName: "carbon", label: "Carbon", level: 1 },
+ { aliases: ["gold", "ouro"], iconName: "gold", label: "Gold", level: 2 },
+ { aliases: ["platinum", "platina"], iconName: "platinum", label: "Platinum", level: 3 },
+ { aliases: ["diamond", "diamante"], iconName: "diamond", label: "Diamond", level: 4 },
+ { aliases: ["ruby", "rubi"], iconName: "ruby", label: "Ruby", level: 5 },
+ { aliases: ["sapphire", "safira"], iconName: "sapphire", label: "Sapphire", level: 6 },
+ { aliases: ["krypton", "cripton", "kryptonita", "criptonita"], iconName: "krypton", label: "Krypton", level: 7 },
+ { aliases: ["vibranium"], iconName: "vibranium", label: "Vibranium", level: 8 },
+];
+
+function normalizePlanValue(plan: string) {
+ return plan
+ .normalize("NFD")
+ .replace(/[\u0300-\u036f]/g, "")
+ .toLowerCase()
+ .replace(/[^a-z]/g, "");
+}
+
+function getPlanDefinition(plan: string) {
+ const normalizedPlan = normalizePlanValue(plan);
+
+ if (!normalizedPlan) return null;
+
+ return PLAN_DEFINITIONS.find(({ aliases }) =>
+ aliases.some(alias => normalizedPlan === alias || normalizedPlan.includes(alias)),
+ ) ?? null;
+}
+
+export function formatPlanLabel(plan: string) {
+ return plan;
+}
+
+export function canAccessSubdomains(plan?: string | null) {
+ const planLevel = plan ? (getPlanDefinition(plan)?.level ?? -1) : -1;
+ return planLevel >= 3;
+}
+
+export function canAccessCustomDomains(plan?: string | null) {
+ const planLevel = plan ? (getPlanDefinition(plan)?.level ?? -1) : -1;
+ return planLevel >= 4;
+}
+
+export function getPlanIconPath(plan: string): TreeItem["iconPath"] | undefined {
+ const iconName = getPlanDefinition(plan)?.iconName;
+
+ if (!iconName) return undefined;
+
+ const iconPath = ["png", "svg"]
+ .map(ext => core.context.asAbsolutePath(join(RESOURCES_DIR, "icons", `${iconName}.${ext}`)))
+ .find(candidate => existsSync(candidate));
+
+ if (!iconPath) return undefined;
+
+ const iconUri = Uri.file(iconPath);
+
+ return {
+ dark: iconUri,
+ light: iconUri,
+ };
+}
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index ee9208db..879d59ee 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -11,6 +11,17 @@ export function getIconPath(iconName: string, iconExt = "svg"): TreeItem["iconPa
};
}
+export function getResourceUri(...pathSegments: string[]) {
+ return Uri.file(core.context.asAbsolutePath(join(...pathSegments)));
+}
+
+export function getThemedResourceIconPath(lightPath: string, darkPath = lightPath): TreeItem["iconPath"] {
+ return {
+ dark: getResourceUri(darkPath),
+ light: getResourceUri(lightPath),
+ };
+}
+
export function compareBooleans(a: boolean, b: boolean) {
let i = 0;
if (a) i--;
diff --git a/yarn.lock b/yarn.lock
index 214ef82c..30640886 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -370,9 +370,9 @@
undici-types "~6.21.0"
"@types/vscode@^1.118.0":
- version "1.118.0"
- resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.118.0.tgz#4a226ddf8faa11a9e6b338555431c4edd3230e4a"
- integrity sha512-Ah6eTlqDcwIMELEVwQMO++rJAFBRz/oLluLD/vWdYrH1KuI9kfpaM+7pg0OvvascgcJy+ghLCERAYouM4QbzGw==
+ version "1.120.0"
+ resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.120.0.tgz#af36ef6e9952aa643e3682468197e319be80b4ae"
+ integrity sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==
"@types/ws@^8.18.1":
version "8.18.1"
@@ -1557,6 +1557,11 @@ imurmurhash@^0.1.4:
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
+inherits@2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+ integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
+
inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
@@ -2259,6 +2264,14 @@ path-type@^3.0.0:
dependencies:
pify "^3.0.0"
+path@^0.12.7:
+ version "0.12.7"
+ resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
+ integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==
+ dependencies:
+ process "^0.11.1"
+ util "^0.10.3"
+
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
@@ -2299,6 +2312,11 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+process@^0.11.1:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+ integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
punycode@^2.1.0, punycode@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
@@ -2955,6 +2973,13 @@ util-deprecate@~1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+util@^0.10.3:
+ version "0.10.4"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
+ integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
+ dependencies:
+ inherits "2.0.3"
+
v8-to-istanbul@^9.0.0:
version "9.3.0"
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175"