feat(plugins): add install-from-url for plugin packages
This commit is contained in:
17 files changed
+436
-51
No files matched your search
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { Check, ChevronRight, CircleAlert, Download, ExternalLink, FileUp, FolderTree, Globe, LayoutGrid, List, Loader2, PackageCheck, Pencil, Plus, RefreshCw, RotateCcw, Search, Settings2, ShieldCheck, Store, Trash2 } from "@lucide/vue";
|
||||
import { Check, ChevronRight, CircleAlert, Download, ExternalLink, FileUp, FolderTree, Globe, LayoutGrid, Link2, List, Loader2, PackageCheck, Pencil, Plus, RefreshCw, RotateCcw, Search, Settings2, ShieldCheck, Store, Trash2 } from "@lucide/vue";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -16,10 +16,11 @@ import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { physicalDropPositionInsideRect } from "@/lib/ai/aiAttachments";
|
||||
import { createFrontendPluginRegistry, pluginConnectionProviderIcon } from "@/lib/plugins/frontendPlugin";
|
||||
import { buildMarketplacePluginListings, filterMarketplacePluginListings, type MarketplacePluginListing } from "@/lib/plugins/pluginMarketplace";
|
||||
import { formatBytes } from "@/lib/database/serverMetrics";
|
||||
import type { PluginCenterFocus } from "@/lib/plugins/pluginCenterNavigation";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import type { InstalledPlugin, PluginRepository, PluginRepositoryCatalogResult, PluginTrustedKey } from "@/types/database";
|
||||
import type { InstalledPlugin, PluginInstallResult, PluginRepository, PluginRepositoryCatalogResult, PluginTrustedKey } from "@/types/database";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -57,6 +58,9 @@ const catalogResults = ref<PluginRepositoryCatalogResult[]>([]);
|
||||
const loading = ref(false);
|
||||
const marketplaceLoading = ref(false);
|
||||
const installing = ref(false);
|
||||
const installUrl = ref("");
|
||||
const urlInstalling = ref(false);
|
||||
const urlDownloadProgress = ref<{ downloaded: number; total: number | null } | null>(null);
|
||||
const marketplaceInstallingKey = ref("");
|
||||
const operating = ref(false);
|
||||
const error = ref("");
|
||||
@@ -339,15 +343,19 @@ async function handleWebPackage(event: Event) {
|
||||
if (file) await installPlugin(file);
|
||||
}
|
||||
|
||||
async function finishInstall(result: PluginInstallResult) {
|
||||
toast(t("pluginPlatform.installSuccess", { name: result.plugin.manifest.name, version: result.plugin.manifest.version }));
|
||||
clearPluginIconCache();
|
||||
installedPlugins.value = await api.listPlugins();
|
||||
selectPlugin(result.plugin.manifest.id);
|
||||
activeSection.value = "installed";
|
||||
}
|
||||
|
||||
async function installPlugin(source: string | File) {
|
||||
installing.value = true;
|
||||
try {
|
||||
const result = await api.installPluginPackage(source, allowUnsigned.value);
|
||||
toast(t("pluginPlatform.installSuccess", { name: result.plugin.manifest.name, version: result.plugin.manifest.version }));
|
||||
clearPluginIconCache();
|
||||
installedPlugins.value = await api.listPlugins();
|
||||
selectPlugin(result.plugin.manifest.id);
|
||||
activeSection.value = "installed";
|
||||
await finishInstall(result);
|
||||
} catch (cause) {
|
||||
toast(cause instanceof Error ? cause.message : String(cause), 8000);
|
||||
} finally {
|
||||
@@ -355,6 +363,54 @@ async function installPlugin(source: string | File) {
|
||||
}
|
||||
}
|
||||
|
||||
function isHttpPackageUrl(value: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function installPluginFromUrl() {
|
||||
const url = installUrl.value.trim();
|
||||
if (!url || installing.value || urlInstalling.value) return;
|
||||
if (!isHttpPackageUrl(url)) return toast(t("pluginPlatform.invalidPackageUrl"));
|
||||
urlInstalling.value = true;
|
||||
urlDownloadProgress.value = { downloaded: 0, total: null };
|
||||
let unlisten: (() => void) | undefined;
|
||||
try {
|
||||
if (isTauriRuntime()) {
|
||||
const { listen } = await import("@tauri-apps/api/event");
|
||||
unlisten = await listen<{ downloaded: number; total: number | null }>("plugin-url-download-progress", ({ payload }) => {
|
||||
urlDownloadProgress.value = payload;
|
||||
});
|
||||
}
|
||||
const result = await api.installPluginPackageFromUrl(url, allowUnsigned.value);
|
||||
installUrl.value = "";
|
||||
await finishInstall(result);
|
||||
} catch (cause) {
|
||||
toast(cause instanceof Error ? cause.message : String(cause), 8000);
|
||||
} finally {
|
||||
unlisten?.();
|
||||
urlInstalling.value = false;
|
||||
urlDownloadProgress.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
const urlProgressPercent = computed(() => {
|
||||
const progress = urlDownloadProgress.value;
|
||||
if (!progress?.total) return null;
|
||||
return Math.min(100, Math.round((progress.downloaded / progress.total) * 100));
|
||||
});
|
||||
|
||||
const urlProgressLabel = computed(() => {
|
||||
const progress = urlDownloadProgress.value;
|
||||
if (!progress) return "";
|
||||
const downloaded = formatBytes(progress.downloaded);
|
||||
return progress.total ? t("pluginPlatform.downloadProgress", { downloaded, total: formatBytes(progress.total) }) : t("pluginPlatform.downloadProgressUnknown", { downloaded });
|
||||
});
|
||||
|
||||
function isPluginPackagePath(path: string): boolean {
|
||||
return /\.dbxp$/i.test(path);
|
||||
}
|
||||
@@ -395,7 +451,7 @@ function onWebDrop(event: DragEvent) {
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (installing.value) return;
|
||||
if (installing.value || urlInstalling.value) return;
|
||||
void installPlugin(file);
|
||||
}
|
||||
|
||||
@@ -422,7 +478,7 @@ function onTauriPluginDrop(event: Event) {
|
||||
}
|
||||
draggingPackage.value = false;
|
||||
const path = payload.paths.find(isPluginPackagePath);
|
||||
if (!inside || !path || installing.value) return;
|
||||
if (!inside || !path || installing.value || urlInstalling.value) return;
|
||||
routedEvent.preventDefault();
|
||||
void installPlugin(path);
|
||||
}
|
||||
@@ -782,10 +838,25 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-8 gap-1.5" :disabled="installing" @click="choosePluginPackage"><Loader2 v-if="installing" class="size-3.5 animate-spin" /><FileUp v-else class="size-3.5" />{{ t("pluginPlatform.installPackage") }}</Button>
|
||||
<Button variant="outline" size="sm" class="h-8 gap-1.5" :disabled="installing || urlInstalling" @click="choosePluginPackage"><Loader2 v-if="installing" class="size-3.5 animate-spin" /><FileUp v-else class="size-3.5" />{{ t("pluginPlatform.installPackage") }}</Button>
|
||||
<div class="flex items-center gap-1.5 text-[11px] text-muted-foreground"><ShieldCheck class="size-3.5 text-emerald-600 dark:text-emerald-400" />{{ t("pluginPlatform.signedPackagesVerifiedAutomatically") }}</div>
|
||||
<div class="flex items-center gap-1.5 text-[11px] text-muted-foreground"><FileUp class="size-3.5" />{{ t("pluginPlatform.dropInstallHint") }}</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="relative min-w-0 flex-1 sm:max-w-md">
|
||||
<Link2 class="pointer-events-none absolute left-2.5 top-2.5 size-3.5 text-muted-foreground" />
|
||||
<Input v-model="installUrl" class="h-8 pl-8 font-mono text-xs" type="url" :disabled="urlInstalling" :placeholder="t('pluginPlatform.installUrlPlaceholder')" @keyup.enter="installPluginFromUrl" />
|
||||
</div>
|
||||
<Button variant="outline" size="sm" class="h-8 gap-1.5" :disabled="installing || urlInstalling || !installUrl.trim()" @click="installPluginFromUrl"
|
||||
><Loader2 v-if="urlInstalling" class="size-3.5 animate-spin" /><Download v-else class="size-3.5" />{{ t("pluginPlatform.installFromUrl") }}</Button
|
||||
>
|
||||
</div>
|
||||
<div v-if="urlInstalling" class="space-y-1.5">
|
||||
<div class="h-1.5 overflow-hidden rounded-full bg-muted">
|
||||
<div class="h-full rounded-full bg-primary transition-[width]" :class="urlProgressPercent === null ? 'w-1/3 animate-pulse' : ''" :style="{ width: urlProgressPercent === null ? undefined : `${urlProgressPercent}%` }" />
|
||||
</div>
|
||||
<div class="text-[10px] text-muted-foreground">{{ urlProgressLabel }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3 rounded-xl border p-4">
|
||||
|
||||
@@ -193,8 +193,8 @@ export default {
|
||||
updateSuccess: "Updated {name} to {version}",
|
||||
noInstalledPlugins: "No plugins installed",
|
||||
browseMarketplace: "Browse Marketplace",
|
||||
localInstallTitle: "Install a local package",
|
||||
localInstallDescription: "Install a signed .dbxp obtained outside the marketplace. DBX verifies package integrity and repository signatures automatically.",
|
||||
localInstallTitle: "Install a plugin package",
|
||||
localInstallDescription: "Install a signed .dbxp obtained outside the marketplace from a local file or URL. DBX verifies package integrity and signatures automatically.",
|
||||
repositoriesTitle: "Plugin repositories",
|
||||
repositoriesDescription: "The official source is signed and managed by DBX. Custom repositories use one repository-level trust key configured below.",
|
||||
repositoryFieldsRequired: "Repository ID, name, and catalog URL are required",
|
||||
@@ -223,6 +223,11 @@ export default {
|
||||
allowUnsignedDevelopmentPackage: "Allow unsigned development packages",
|
||||
allowUnsignedDevelopmentPackageDescription: "Applies only to manual .dbxp installation and never relaxes official marketplace verification.",
|
||||
installPackage: "Install .dbxp",
|
||||
installFromUrl: "Install from URL",
|
||||
installUrlPlaceholder: "https://example.com/plugin.dbxp",
|
||||
invalidPackageUrl: "Enter a valid http:// or https:// plugin package URL",
|
||||
downloadProgress: "Downloading {downloaded} of {total}",
|
||||
downloadProgressUnknown: "Downloading {downloaded}",
|
||||
dropToInstall: "Drop to install the plugin package",
|
||||
dropInstallHint: "You can also drop a .dbxp file onto this page",
|
||||
packageFileType: "DBX plugin package",
|
||||
|
||||
@@ -190,8 +190,8 @@ export default withEnglishFallback({
|
||||
updateSuccess: "Se actualizó {name} a {version}",
|
||||
noInstalledPlugins: "No hay plugins instalados",
|
||||
browseMarketplace: "Explorar Marketplace",
|
||||
localInstallTitle: "Instalar un paquete local",
|
||||
localInstallDescription: "Instala un .dbxp firmado obtenido fuera del marketplace. DBX verifica automáticamente la integridad y la firma del repositorio.",
|
||||
localInstallTitle: "Instalar un paquete de plugin",
|
||||
localInstallDescription: "Instala un .dbxp firmado obtenido fuera del marketplace desde un archivo local o URL. DBX verifica automáticamente la integridad y la firma.",
|
||||
repositoriesTitle: "Repositorios de plugins",
|
||||
repositoriesDescription: "DBX administra la fuente oficial. Los repositorios propios o de terceros usan la configuración de confianza del repositorio.",
|
||||
repositoryFieldsRequired: "El ID, el nombre y la URL del catálogo son obligatorios",
|
||||
@@ -216,6 +216,11 @@ export default withEnglishFallback({
|
||||
allowUnsignedDevelopmentPackage: "Permitir paquetes de desarrollo sin firmar",
|
||||
allowUnsignedDevelopmentPackageDescription: "Solo afecta a la instalación manual de .dbxp y nunca reduce la verificación del marketplace oficial.",
|
||||
installPackage: "Instalar .dbxp",
|
||||
installFromUrl: "Instalar desde URL",
|
||||
installUrlPlaceholder: "https://example.com/plugin.dbxp",
|
||||
invalidPackageUrl: "Introduce una URL de paquete de plugin http(s) válida",
|
||||
downloadProgress: "Descargando {downloaded} de {total}",
|
||||
downloadProgressUnknown: "Descargando {downloaded}",
|
||||
dropToInstall: "Suelta para instalar el paquete del plugin",
|
||||
dropInstallHint: "También puedes soltar un archivo .dbxp en esta página",
|
||||
packageFileType: "Paquete de plugin de DBX",
|
||||
|
||||
@@ -189,8 +189,8 @@ export default withEnglishFallback({
|
||||
updateSuccess: "{name} aggiornato alla versione {version}",
|
||||
noInstalledPlugins: "Nessun plugin installato",
|
||||
browseMarketplace: "Sfoglia Marketplace",
|
||||
localInstallTitle: "Installa un pacchetto locale",
|
||||
localInstallDescription: "Installa un .dbxp firmato ottenuto fuori dal marketplace. DBX verifica automaticamente integrità e firma del repository.",
|
||||
localInstallTitle: "Installa un pacchetto plugin",
|
||||
localInstallDescription: "Installa un .dbxp firmato ottenuto fuori dal marketplace da un file locale o da un URL. DBX verifica automaticamente integrità e firma.",
|
||||
repositoriesTitle: "Repository dei plugin",
|
||||
repositoriesDescription: "La sorgente ufficiale è gestita da DBX. I repository self-hosted o di terze parti usano le impostazioni di attendibilità del repository.",
|
||||
repositoryFieldsRequired: "ID, nome e URL del catalogo sono obbligatori",
|
||||
@@ -215,6 +215,11 @@ export default withEnglishFallback({
|
||||
allowUnsignedDevelopmentPackage: "Consenti pacchetti di sviluppo non firmati",
|
||||
allowUnsignedDevelopmentPackageDescription: "Si applica solo all'installazione manuale di .dbxp e non riduce mai la verifica del marketplace ufficiale.",
|
||||
installPackage: "Installa .dbxp",
|
||||
installFromUrl: "Installa da URL",
|
||||
installUrlPlaceholder: "https://example.com/plugin.dbxp",
|
||||
invalidPackageUrl: "Inserisci un URL http(s) valido per il pacchetto del plugin",
|
||||
downloadProgress: "Download di {downloaded} su {total}",
|
||||
downloadProgressUnknown: "Download di {downloaded}",
|
||||
dropToInstall: "Rilascia per installare il pacchetto del plugin",
|
||||
dropInstallHint: "Puoi anche rilasciare un file .dbxp su questa pagina",
|
||||
packageFileType: "Pacchetto plugin DBX",
|
||||
|
||||
@@ -190,8 +190,8 @@ export default withEnglishFallback({
|
||||
updateSuccess: "{name} を {version} に更新しました",
|
||||
noInstalledPlugins: "プラグインはまだありません",
|
||||
browseMarketplace: "マーケットプレイスを見る",
|
||||
localInstallTitle: "ローカルパッケージをインストール",
|
||||
localInstallDescription: "マーケットプレイス外で入手した署名済み .dbxp をインストールします。DBX が整合性とリポジトリ署名を自動検証します。",
|
||||
localInstallTitle: "プラグインパッケージをインストール",
|
||||
localInstallDescription: "マーケットプレイス外で入手した署名済み .dbxp をローカルファイルまたは URL からインストールします。DBX が整合性と署名を自動検証します。",
|
||||
repositoriesTitle: "プラグインリポジトリ",
|
||||
repositoriesDescription: "公式ソースは DBX が管理します。セルフホストまたは第三者のリポジトリでは、リポジトリの信頼設定を使用します。",
|
||||
repositoryFieldsRequired: "リポジトリ ID、名前、カタログ URL は必須です",
|
||||
@@ -216,6 +216,11 @@ export default withEnglishFallback({
|
||||
allowUnsignedDevelopmentPackage: "未署名の開発パッケージを許可",
|
||||
allowUnsignedDevelopmentPackageDescription: "手動での .dbxp インストールにのみ適用され、公式マーケットプレイスの検証は緩和されません。",
|
||||
installPackage: ".dbxp をインストール",
|
||||
installFromUrl: "URL からインストール",
|
||||
installUrlPlaceholder: "https://example.com/plugin.dbxp",
|
||||
invalidPackageUrl: "有効な http(s) プラグインパッケージ URL を入力してください",
|
||||
downloadProgress: "ダウンロード中 {downloaded} / {total}",
|
||||
downloadProgressUnknown: "ダウンロード中 {downloaded}",
|
||||
dropToInstall: "ドロップしてプラグインをインストール",
|
||||
dropInstallHint: ".dbxp ファイルをこのページにドロップしてもインストールできます",
|
||||
packageFileType: "DBX プラグインパッケージ",
|
||||
|
||||
@@ -194,8 +194,8 @@ export default withEnglishFallback({
|
||||
updateSuccess: "{name} 업데이트 완료: {version}",
|
||||
noInstalledPlugins: "설치된 플러그인이 없습니다",
|
||||
browseMarketplace: "마켓플레이스 둘러보기",
|
||||
localInstallTitle: "로컬 패키지 설치",
|
||||
localInstallDescription: "마켓플레이스 외부에서 받은 서명된 .dbxp를 설치합니다. DBX가 패키지 무결성과 저장소 서명을 자동으로 검증합니다.",
|
||||
localInstallTitle: "플러그인 패키지 설치",
|
||||
localInstallDescription: "마켓플레이스 외부에서 받은 서명된 .dbxp를 로컬 파일이나 URL에서 설치합니다. DBX가 패키지 무결성과 서명을 자동으로 검증합니다.",
|
||||
repositoriesTitle: "플러그인 저장소",
|
||||
repositoriesDescription: "공식 소스는 DBX에서 관리합니다. 자체 호스팅 또는 타사 저장소는 저장소 신뢰 설정을 사용합니다.",
|
||||
repositoryFieldsRequired: "저장소 ID, 이름 및 카탈로그 URL은 필수입니다",
|
||||
@@ -224,6 +224,11 @@ export default withEnglishFallback({
|
||||
allowUnsignedDevelopmentPackage: "서명되지 않은 개발 패키지 허용",
|
||||
allowUnsignedDevelopmentPackageDescription: "수동 .dbxp 설치에만 적용되며 공식 마켓플레이스 검증은 완화하지 않습니다.",
|
||||
installPackage: ".dbxp 설치",
|
||||
installFromUrl: "URL에서 설치",
|
||||
installUrlPlaceholder: "https://example.com/plugin.dbxp",
|
||||
invalidPackageUrl: "유효한 http(s) 플러그인 패키지 URL을 입력하세요",
|
||||
downloadProgress: "다운로드 중 {downloaded} / {total}",
|
||||
downloadProgressUnknown: "다운로드 중 {downloaded}",
|
||||
dropToInstall: "놓아서 플러그인 패키지 설치",
|
||||
dropInstallHint: ".dbxp 파일을 이 페이지에 끌어다 놓아도 설치할 수 있습니다",
|
||||
packageFileType: "DBX 플러그인 패키지",
|
||||
|
||||
@@ -190,8 +190,8 @@ export default withEnglishFallback({
|
||||
updateSuccess: "{name} atualizado para {version}",
|
||||
noInstalledPlugins: "Nenhum plugin instalado",
|
||||
browseMarketplace: "Explorar Marketplace",
|
||||
localInstallTitle: "Instalar pacote local",
|
||||
localInstallDescription: "Instale um .dbxp assinado obtido fora do marketplace. O DBX verifica automaticamente a integridade e a assinatura do repositório.",
|
||||
localInstallTitle: "Instalar pacote de plugin",
|
||||
localInstallDescription: "Instale um .dbxp assinado obtido fora do marketplace a partir de um arquivo local ou URL. O DBX verifica automaticamente a integridade e a assinatura.",
|
||||
repositoriesTitle: "Repositórios de plugins",
|
||||
repositoriesDescription: "A fonte oficial é gerenciada pelo DBX. Repositórios próprios ou de terceiros usam as configurações de confiança do repositório.",
|
||||
repositoryFieldsRequired: "ID, nome e URL do catálogo são obrigatórios",
|
||||
@@ -216,6 +216,11 @@ export default withEnglishFallback({
|
||||
allowUnsignedDevelopmentPackage: "Permitir pacotes de desenvolvimento não assinados",
|
||||
allowUnsignedDevelopmentPackageDescription: "Afeta apenas a instalação manual de .dbxp e nunca reduz a verificação do marketplace oficial.",
|
||||
installPackage: "Instalar .dbxp",
|
||||
installFromUrl: "Instalar de URL",
|
||||
installUrlPlaceholder: "https://example.com/plugin.dbxp",
|
||||
invalidPackageUrl: "Insira uma URL http(s) válida do pacote do plugin",
|
||||
downloadProgress: "Baixando {downloaded} de {total}",
|
||||
downloadProgressUnknown: "Baixando {downloaded}",
|
||||
dropToInstall: "Solte para instalar o pacote do plugin",
|
||||
dropInstallHint: "Você também pode soltar um arquivo .dbxp nesta página",
|
||||
packageFileType: "Pacote de plugin do DBX",
|
||||
|
||||
@@ -113,8 +113,8 @@ export default withEnglishFallback({
|
||||
updateSuccess: "已将 {name} 更新到 {version}",
|
||||
noInstalledPlugins: "尚未安装插件",
|
||||
browseMarketplace: "浏览插件商店",
|
||||
localInstallTitle: "安装本地插件包",
|
||||
localInstallDescription: "安装从插件商店之外获得的已签名 .dbxp,DBX 会自动校验包完整性和仓库签名。",
|
||||
localInstallTitle: "安装插件包",
|
||||
localInstallDescription: "从本地文件或 URL 安装插件商店之外的 .dbxp 插件包,DBX 会自动校验包完整性和签名。",
|
||||
repositoriesTitle: "插件仓库",
|
||||
repositoriesDescription: "官方源由 DBX 管理;添加自托管或第三方仓库后,才会显示对应的仓库信任设置。",
|
||||
repositoryFieldsRequired: "仓库 ID、名称和目录地址不能为空",
|
||||
@@ -139,6 +139,11 @@ export default withEnglishFallback({
|
||||
allowUnsignedDevelopmentPackage: "允许安装未签名开发包",
|
||||
allowUnsignedDevelopmentPackageDescription: "仅影响手动安装的 .dbxp,不会放宽官方插件商店的签名校验。",
|
||||
installPackage: "安装 .dbxp",
|
||||
installFromUrl: "从 URL 安装",
|
||||
installUrlPlaceholder: "https://example.com/plugin.dbxp",
|
||||
invalidPackageUrl: "请输入有效的 http(s) 插件包链接",
|
||||
downloadProgress: "正在下载 {downloaded} / {total}",
|
||||
downloadProgressUnknown: "正在下载 {downloaded}",
|
||||
dropToInstall: "释放以安装插件包",
|
||||
dropInstallHint: "也可拖放 .dbxp 到插件中心页面安装",
|
||||
packageFileType: "DBX 插件包",
|
||||
|
||||
@@ -190,8 +190,8 @@ export default withEnglishFallback({
|
||||
updateSuccess: "已將 {name} 更新至 {version}",
|
||||
noInstalledPlugins: "尚未安裝外掛",
|
||||
browseMarketplace: "瀏覽外掛商店",
|
||||
localInstallTitle: "安裝本機外掛套件",
|
||||
localInstallDescription: "安裝從外掛商店之外取得的已簽署 .dbxp,DBX 會自動驗證套件完整性與倉庫簽章。",
|
||||
localInstallTitle: "安裝外掛套件",
|
||||
localInstallDescription: "從本機檔案或 URL 安裝外掛商店之外的 .dbxp 外掛套件,DBX 會自動驗證套件完整性與簽章。",
|
||||
repositoriesTitle: "外掛倉庫",
|
||||
repositoriesDescription: "官方來源由 DBX 管理;加入自託管或第三方倉庫後,才會顯示對應的倉庫信任設定。",
|
||||
repositoryFieldsRequired: "倉庫 ID、名稱和目錄網址皆為必填",
|
||||
@@ -216,6 +216,11 @@ export default withEnglishFallback({
|
||||
allowUnsignedDevelopmentPackage: "允許安裝未簽署開發套件",
|
||||
allowUnsignedDevelopmentPackageDescription: "僅影響手動安裝的 .dbxp,不會放寬官方外掛商店的簽章驗證。",
|
||||
installPackage: "安裝 .dbxp",
|
||||
installFromUrl: "從 URL 安裝",
|
||||
installUrlPlaceholder: "https://example.com/plugin.dbxp",
|
||||
invalidPackageUrl: "請輸入有效的 http(s) 外掛套件連結",
|
||||
downloadProgress: "正在下載 {downloaded} / {total}",
|
||||
downloadProgressUnknown: "正在下載 {downloaded}",
|
||||
dropToInstall: "放開以安裝外掛套件",
|
||||
dropInstallHint: "也可拖放 .dbxp 到外掛中心頁面安裝",
|
||||
packageFileType: "DBX 外掛套件",
|
||||
|
||||
@@ -88,6 +88,7 @@ export const removePluginRepository = forward("removePluginRepository");
|
||||
export const fetchPluginMarketplaceCatalogs = forward("fetchPluginMarketplaceCatalogs");
|
||||
export const installMarketplacePlugin = forward("installMarketplacePlugin");
|
||||
export const installPluginPackage = forward("installPluginPackage");
|
||||
export const installPluginPackageFromUrl = forward("installPluginPackageFromUrl");
|
||||
export const rollbackPlugin = forward("rollbackPlugin");
|
||||
export const uninstallPlugin = forward("uninstallPlugin");
|
||||
export const activatePlugin = forward("activatePlugin");
|
||||
|
||||
@@ -590,6 +590,22 @@ export async function installPluginPackage(pathOrFile: string | File, allowUnsig
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function installPluginPackageFromUrl(url: string, allowUnsigned = false): Promise<PluginInstallResult> {
|
||||
let blob: Blob;
|
||||
let fileName: string;
|
||||
try {
|
||||
fileName = new URL(url).pathname.split("/").pop() || "plugin.dbxp";
|
||||
} catch {
|
||||
fileName = "plugin.dbxp";
|
||||
}
|
||||
blob = await (await fetch(url)).blob();
|
||||
const formData = new FormData();
|
||||
formData.append("file", blob, fileName);
|
||||
const response = await fetch(apiUrl(`/api/plugins/install?allow_unsigned=${allowUnsigned}&from_url=true`), { method: "POST", body: formData });
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function rollbackPlugin(pluginId: string): Promise<PluginRollbackResult> {
|
||||
return post("/api/plugins/rollback", { plugin_id: pluginId });
|
||||
}
|
||||
|
||||
@@ -2294,6 +2294,10 @@ export async function installPluginPackage(pathOrFile: string | File, allowUnsig
|
||||
return invoke("install_plugin_package", { path: pathOrFile, allowUnsigned });
|
||||
}
|
||||
|
||||
export async function installPluginPackageFromUrl(url: string, allowUnsigned = false): Promise<PluginInstallResult> {
|
||||
return invoke("install_plugin_package_from_url", { url, allowUnsigned });
|
||||
}
|
||||
|
||||
export async function rollbackPlugin(pluginId: string): Promise<PluginRollbackResult> {
|
||||
return invoke("rollback_plugin", { pluginId });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user