mirror of
https://github.com/Eugeny/tabby.git
synced 2025-08-31 21:51:50 +00:00
project rename
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
.alert.alert-danger(*ngIf='errorMessage')
|
||||
strong Error in {{erroredPlugin}}:
|
||||
pre {{errorMessage}}
|
||||
|
||||
.d-flex
|
||||
h3.mb-1 Installed
|
||||
button.btn.btn-outline-secondary.btn-sm.ml-auto((click)='openPluginsFolder()')
|
||||
i.fas.fa-folder
|
||||
span Plugins folder
|
||||
|
||||
.list-group.list-group-flush.mt-2
|
||||
.list-group-item.d-flex.align-items-center(*ngFor='let plugin of pluginManager.installedPlugins')
|
||||
toggle(
|
||||
[ngModel]='isPluginEnabled(plugin)',
|
||||
(ngModelChange)='togglePlugin(plugin)',
|
||||
[disabled]='!canDisablePlugin(plugin)'
|
||||
)
|
||||
|
||||
.mr-auto.d-flex.flex-column
|
||||
div
|
||||
strong {{plugin.name}}
|
||||
small.text-muted.ml-1(*ngIf='!plugin.isBuiltin') {{plugin.version}} / {{plugin.author}}
|
||||
small.text-muted.ml-1(*ngIf='plugin.isBuiltin') Built-in
|
||||
small.text-warning.ml-1(*ngIf='!isPluginEnabled(plugin)') Disabled
|
||||
a.text-muted.mb-0((click)='showPluginInfo(plugin)')
|
||||
small {{plugin.description}}
|
||||
|
||||
button.btn.btn-primary.ml-2(
|
||||
*ngIf='knownUpgrades[plugin.name]',
|
||||
(click)='upgradePlugin(plugin)',
|
||||
[disabled]='busy.has(plugin.name)'
|
||||
)
|
||||
i.fas.fa-fw.fa-arrow-up(*ngIf='busy.get(plugin.name) != BusyState.Installing')
|
||||
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy.get(plugin.name) == BusyState.Installing')
|
||||
span Upgrade ({{knownUpgrades[plugin.name].version}})
|
||||
|
||||
button.btn.btn-link.text-danger.ml-2(
|
||||
(click)='uninstallPlugin(plugin)',
|
||||
*ngIf='!plugin.isBuiltin',
|
||||
[disabled]='busy.has(plugin.name)'
|
||||
)
|
||||
i.fas.fa-fw.fa-trash(*ngIf='busy.get(plugin.name) != BusyState.Uninstalling')
|
||||
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy.get(plugin.name) == BusyState.Uninstalling')
|
||||
|
||||
div
|
||||
h3.mt-4 Available
|
||||
|
||||
.input-group.mb-3
|
||||
.input-group-prepend
|
||||
.input-group-text
|
||||
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='!availablePluginsReady')
|
||||
i.fas.fa-fw.fa-search(*ngIf='availablePluginsReady')
|
||||
input.form-control(
|
||||
type='text',
|
||||
[(ngModel)]='_1',
|
||||
(ngModelChange)='searchAvailable(_1)',
|
||||
placeholder='Search plugins'
|
||||
)
|
||||
|
||||
.list-group.list-group-flush.mb-4(*ngIf='availablePlugins$')
|
||||
ng-container(*ngFor='let plugin of (availablePlugins$|async)')
|
||||
.list-group-item.d-flex.align-items-center(*ngIf='!isAlreadyInstalled(plugin)')
|
||||
button.btn.btn-primary.mr-3(
|
||||
(click)='installPlugin(plugin)',
|
||||
[disabled]='busy.has(plugin.name)'
|
||||
)
|
||||
i.fas.fa-fw.fa-download(*ngIf='busy.get(plugin.name) != BusyState.Installing')
|
||||
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy.get(plugin.name) == BusyState.Installing')
|
||||
|
||||
div((click)='showPluginInfo(plugin)')
|
||||
div
|
||||
strong {{plugin.name}}
|
||||
small.text-muted.ml-1 {{plugin.version}}
|
||||
small.text-muted.ml-1(*ngIf='!plugin.isOfficial') by {{plugin.author}}
|
||||
small.text-success.ml-1(*ngIf='plugin.isOfficial')
|
||||
i.fas.fa-check
|
||||
span.ml-1 Official
|
||||
small.text-muted {{plugin.description}}
|
@@ -0,0 +1,8 @@
|
||||
.appearance-preview {
|
||||
padding: 10px 20px;
|
||||
margin: 0 0 10px;
|
||||
overflow: hidden;
|
||||
span {
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
@@ -0,0 +1,134 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { BehaviorSubject, Observable } from 'rxjs'
|
||||
import { debounceTime, distinctUntilChanged, first, tap, flatMap, map } from 'rxjs/operators'
|
||||
import semverGt from 'semver/functions/gt'
|
||||
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ConfigService, PlatformService, PluginInfo } from 'tabby-core'
|
||||
import { PluginManagerService } from '../services/pluginManager.service'
|
||||
|
||||
enum BusyState { Installing = 'Installing', Uninstalling = 'Uninstalling' }
|
||||
|
||||
const FORCE_ENABLE = ['tabby-core', 'tabby-settings']
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
template: require('./pluginsSettingsTab.component.pug'),
|
||||
styles: [require('./pluginsSettingsTab.component.scss')],
|
||||
})
|
||||
export class PluginsSettingsTabComponent {
|
||||
BusyState = BusyState
|
||||
@Input() availablePlugins$: Observable<PluginInfo[]>
|
||||
@Input() availablePluginsQuery$ = new BehaviorSubject<string>('')
|
||||
@Input() availablePluginsReady = false
|
||||
@Input() knownUpgrades: Record<string, PluginInfo|null> = {}
|
||||
@Input() busy = new Map<string, BusyState>()
|
||||
@Input() erroredPlugin: string
|
||||
@Input() errorMessage: string
|
||||
|
||||
constructor (
|
||||
private config: ConfigService,
|
||||
private platform: PlatformService,
|
||||
public pluginManager: PluginManagerService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.availablePlugins$ = this.availablePluginsQuery$
|
||||
.asObservable()
|
||||
.pipe(
|
||||
debounceTime(200),
|
||||
distinctUntilChanged(),
|
||||
flatMap(query => {
|
||||
this.availablePluginsReady = false
|
||||
return this.pluginManager.listAvailable(query).pipe(tap(() => {
|
||||
this.availablePluginsReady = true
|
||||
}))
|
||||
})
|
||||
)
|
||||
this.availablePlugins$.pipe(first(), map((plugins: PluginInfo[]) => {
|
||||
plugins.sort((a, b) => a.name > b.name ? 1 : -1)
|
||||
return plugins
|
||||
})).subscribe(available => {
|
||||
for (const plugin of this.pluginManager.installedPlugins) {
|
||||
this.knownUpgrades[plugin.name] = available.find(x => x.name === plugin.name && semverGt(x.version, plugin.version)) || null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
openPluginsFolder (): void {
|
||||
this.platform.openPath(this.pluginManager.userPluginsPath)
|
||||
}
|
||||
|
||||
searchAvailable (query: string) {
|
||||
this.availablePluginsQuery$.next(query)
|
||||
}
|
||||
|
||||
isAlreadyInstalled (plugin: PluginInfo): boolean {
|
||||
return this.pluginManager.installedPlugins.some(x => x.name === plugin.name)
|
||||
}
|
||||
|
||||
async installPlugin (plugin: PluginInfo): Promise<void> {
|
||||
this.busy.set(plugin.name, BusyState.Installing)
|
||||
try {
|
||||
await this.pluginManager.installPlugin(plugin)
|
||||
this.busy.delete(plugin.name)
|
||||
this.config.requestRestart()
|
||||
} catch (err) {
|
||||
this.erroredPlugin = plugin.name
|
||||
this.errorMessage = err
|
||||
this.busy.delete(plugin.name)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async uninstallPlugin (plugin: PluginInfo): Promise<void> {
|
||||
this.busy.set(plugin.name, BusyState.Uninstalling)
|
||||
try {
|
||||
await this.pluginManager.uninstallPlugin(plugin)
|
||||
this.busy.delete(plugin.name)
|
||||
this.config.requestRestart()
|
||||
} catch (err) {
|
||||
this.erroredPlugin = plugin.name
|
||||
this.errorMessage = err
|
||||
this.busy.delete(plugin.name)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async upgradePlugin (plugin: PluginInfo): Promise<void> {
|
||||
return this.installPlugin(this.knownUpgrades[plugin.name]!)
|
||||
}
|
||||
|
||||
showPluginInfo (plugin: PluginInfo) {
|
||||
this.platform.openExternal('https://www.npmjs.com/package/' + plugin.packageName)
|
||||
}
|
||||
|
||||
isPluginEnabled (plugin: PluginInfo) {
|
||||
return !this.config.store.pluginBlacklist.includes(plugin.name)
|
||||
}
|
||||
|
||||
canDisablePlugin (plugin: PluginInfo) {
|
||||
return !FORCE_ENABLE.includes(plugin.packageName)
|
||||
}
|
||||
|
||||
togglePlugin (plugin: PluginInfo) {
|
||||
if (this.isPluginEnabled(plugin)) {
|
||||
this.disablePlugin(plugin)
|
||||
} else {
|
||||
this.enablePlugin(plugin)
|
||||
}
|
||||
}
|
||||
|
||||
enablePlugin (plugin: PluginInfo) {
|
||||
this.config.store.pluginBlacklist = this.config.store.pluginBlacklist.filter(x => x !== plugin.name)
|
||||
this.config.save()
|
||||
this.config.requestRestart()
|
||||
}
|
||||
|
||||
disablePlugin (plugin: PluginInfo) {
|
||||
this.config.store.pluginBlacklist = [...this.config.store.pluginBlacklist, plugin.name]
|
||||
this.config.save()
|
||||
this.config.requestRestart()
|
||||
}
|
||||
}
|
32
tabby-plugin-manager/src/index.ts
Normal file
32
tabby-plugin-manager/src/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
import TabbyCorePlugin from 'tabby-core'
|
||||
import { SettingsTabProvider } from 'tabby-settings'
|
||||
|
||||
import { PluginsSettingsTabComponent } from './components/pluginsSettingsTab.component'
|
||||
import { PluginManagerService } from './services/pluginManager.service'
|
||||
import { PluginsSettingsTabProvider } from './settings'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
NgbModule,
|
||||
TabbyCorePlugin,
|
||||
],
|
||||
providers: [
|
||||
{ provide: SettingsTabProvider, useClass: PluginsSettingsTabProvider, multi: true },
|
||||
],
|
||||
entryComponents: [
|
||||
PluginsSettingsTabComponent,
|
||||
],
|
||||
declarations: [
|
||||
PluginsSettingsTabComponent,
|
||||
],
|
||||
})
|
||||
export default class PluginManagerModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
|
||||
export { PluginManagerService }
|
79
tabby-plugin-manager/src/services/pluginManager.service.ts
Normal file
79
tabby-plugin-manager/src/services/pluginManager.service.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import axios from 'axios'
|
||||
import { Observable, from, forkJoin } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { Logger, LogService, PlatformService, BOOTSTRAP_DATA, BootstrapData, PluginInfo } from 'tabby-core'
|
||||
|
||||
const OFFICIAL_NPM_ACCOUNT = 'eugenepankov'
|
||||
|
||||
const BLACKLIST = [
|
||||
'terminus-shell-selector', // superseded by profiles
|
||||
]
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PluginManagerService {
|
||||
logger: Logger
|
||||
userPluginsPath: string
|
||||
installedPlugins: PluginInfo[]
|
||||
|
||||
private constructor (
|
||||
log: LogService,
|
||||
private platform: PlatformService,
|
||||
@Inject(BOOTSTRAP_DATA) bootstrapData: BootstrapData,
|
||||
) {
|
||||
this.logger = log.create('pluginManager')
|
||||
this.installedPlugins = bootstrapData.installedPlugins
|
||||
this.userPluginsPath = bootstrapData.userPluginsPath
|
||||
}
|
||||
|
||||
listAvailable (query?: string): Observable<PluginInfo[]> {
|
||||
return forkJoin(
|
||||
this._listAvailableInternal('tabby-', 'tabby-plugin', query),
|
||||
this._listAvailableInternal('terminus-', 'terminus-plugin', query),
|
||||
).pipe(map(x => x.reduce((a, b) => a.concat(b), [])))
|
||||
}
|
||||
|
||||
_listAvailableInternal (namePrefix: string, keyword: string, query?: string): Observable<PluginInfo[]> {
|
||||
return from(
|
||||
axios.get(`https://www.npmjs.com/search?q=keywords%3A${keyword}+${encodeURIComponent(query ?? '')}&from=0&size=1000`, {
|
||||
headers: {
|
||||
'x-spiferack': '1',
|
||||
},
|
||||
})
|
||||
).pipe(
|
||||
map(response => response.data.objects.map(item => ({
|
||||
name: item.package.name.substring(namePrefix.length),
|
||||
packageName: item.package.name,
|
||||
description: item.package.description,
|
||||
version: item.package.version,
|
||||
homepage: item.package.links.homepage,
|
||||
author: (item.package.author || {}).name,
|
||||
isOfficial: item.package.publisher.name === OFFICIAL_NPM_ACCOUNT,
|
||||
}))),
|
||||
map(plugins => plugins.filter(x => x.packageName.startsWith(namePrefix))),
|
||||
map(plugins => plugins.filter(x => !BLACKLIST.includes(x.packageName))),
|
||||
map(plugins => plugins.sort((a, b) => a.name.localeCompare(b.name))),
|
||||
)
|
||||
}
|
||||
|
||||
async installPlugin (plugin: PluginInfo): Promise<void> {
|
||||
try {
|
||||
await this.platform.installPlugin(plugin.packageName, plugin.version)
|
||||
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
|
||||
this.installedPlugins.push(plugin)
|
||||
} catch (err) {
|
||||
this.logger.error(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async uninstallPlugin (plugin: PluginInfo): Promise<void> {
|
||||
try {
|
||||
await this.platform.uninstallPlugin(plugin.packageName)
|
||||
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
|
||||
} catch (err) {
|
||||
this.logger.error(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
15
tabby-plugin-manager/src/settings.ts
Normal file
15
tabby-plugin-manager/src/settings.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { SettingsTabProvider } from 'tabby-settings'
|
||||
|
||||
import { PluginsSettingsTabComponent } from './components/pluginsSettingsTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class PluginsSettingsTabProvider extends SettingsTabProvider {
|
||||
id = 'plugins'
|
||||
title = 'Plugins'
|
||||
|
||||
getComponentType (): any {
|
||||
return PluginsSettingsTabComponent
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user