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"