mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-08 21:40:03 +00:00
experimental config sync
This commit is contained in:
parent
99ab8dacd4
commit
69115fb77a
@ -31,3 +31,4 @@ enableAutomaticUpdates: true
|
|||||||
version: 1
|
version: 1
|
||||||
vault: null
|
vault: null
|
||||||
encrypted: false
|
encrypted: false
|
||||||
|
enableExperimentalFeatures: false
|
||||||
|
@ -136,9 +136,11 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
|||||||
profilesService: ProfilesService,
|
profilesService: ProfilesService,
|
||||||
) {
|
) {
|
||||||
app.ready$.subscribe(() => {
|
app.ready$.subscribe(() => {
|
||||||
if (config.store.enableWelcomeTab) {
|
config.ready$.toPromise().then(() => {
|
||||||
app.openNewTabRaw({ type: WelcomeTabComponent })
|
if (config.store.enableWelcomeTab) {
|
||||||
}
|
app.openNewTabRaw({ type: WelcomeTabComponent })
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
platform.setErrorHandler(err => {
|
platform.setErrorHandler(err => {
|
||||||
|
@ -194,7 +194,6 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async save (): Promise<void> {
|
async save (): Promise<void> {
|
||||||
this.store.__cleanup()
|
|
||||||
// Scrub undefined values
|
// Scrub undefined values
|
||||||
let cleanStore = JSON.parse(JSON.stringify(this._store))
|
let cleanStore = JSON.parse(JSON.stringify(this._store))
|
||||||
cleanStore = await this.maybeEncryptConfig(cleanStore)
|
cleanStore = await this.maybeEncryptConfig(cleanStore)
|
||||||
@ -238,7 +237,7 @@ export class ConfigService {
|
|||||||
const module = imp.ngModule || imp
|
const module = imp.ngModule || imp
|
||||||
if (module.ɵinj?.providers) {
|
if (module.ɵinj?.providers) {
|
||||||
this.servicesCache[module.pluginName] = module.ɵinj.providers.map(provider => {
|
this.servicesCache[module.pluginName] = module.ɵinj.providers.map(provider => {
|
||||||
return provider.useClass || provider
|
return provider.useClass ?? provider.useExisting ?? provider
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -382,10 +381,12 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
delete decryptedVault.config.vault
|
delete decryptedVault.config.vault
|
||||||
delete decryptedVault.config.encrypted
|
delete decryptedVault.config.encrypted
|
||||||
|
delete decryptedVault.config.configSync
|
||||||
return {
|
return {
|
||||||
...decryptedVault.config,
|
...decryptedVault.config,
|
||||||
vault: store.vault,
|
vault: store.vault,
|
||||||
encrypted: store.encrypted,
|
encrypted: store.encrypted,
|
||||||
|
configSync: store.configSync,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,9 +401,11 @@ export class ConfigService {
|
|||||||
vault.config = { ...store }
|
vault.config = { ...store }
|
||||||
delete vault.config.vault
|
delete vault.config.vault
|
||||||
delete vault.config.encrypted
|
delete vault.config.encrypted
|
||||||
|
delete vault.config.configSync
|
||||||
return {
|
return {
|
||||||
vault: await this.vault.encrypt(vault),
|
vault: await this.vault.encrypt(vault),
|
||||||
encrypted: true,
|
encrypted: true,
|
||||||
|
configSync: store.configSync,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import { ElectronHostWindow } from './services/hostWindow.service'
|
|||||||
import { ElectronFileProvider } from './services/fileProvider.service'
|
import { ElectronFileProvider } from './services/fileProvider.service'
|
||||||
import { ElectronHostAppService } from './services/hostApp.service'
|
import { ElectronHostAppService } from './services/hostApp.service'
|
||||||
import { ElectronService } from './services/electron.service'
|
import { ElectronService } from './services/electron.service'
|
||||||
import { ElectronHotkeyProvider } from './hotkeys'
|
// import { ElectronHotkeyProvider } from './hotkeys'
|
||||||
import { ElectronConfigProvider } from './config'
|
import { ElectronConfigProvider } from './config'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -24,7 +24,7 @@ import { ElectronConfigProvider } from './config'
|
|||||||
{ provide: LogService, useClass: ElectronLogService },
|
{ provide: LogService, useClass: ElectronLogService },
|
||||||
{ provide: UpdaterService, useClass: ElectronUpdaterService },
|
{ provide: UpdaterService, useClass: ElectronUpdaterService },
|
||||||
{ provide: DockingService, useClass: ElectronDockingService },
|
{ provide: DockingService, useClass: ElectronDockingService },
|
||||||
{ provide: HotkeyProvider, useClass: ElectronHotkeyProvider, multi: true },
|
// { provide: HotkeyProvider, useClass: ElectronHotkeyProvider, multi: true },
|
||||||
{ provide: ConfigProvider, useClass: ElectronConfigProvider, multi: true },
|
{ provide: ConfigProvider, useClass: ElectronConfigProvider, multi: true },
|
||||||
{ provide: FileProvider, useClass: ElectronFileProvider, multi: true },
|
{ provide: FileProvider, useClass: ElectronFileProvider, multi: true },
|
||||||
],
|
],
|
||||||
|
@ -8,7 +8,7 @@ import { PluginManagerService } from '../services/pluginManager.service'
|
|||||||
|
|
||||||
enum BusyState { Installing = 'Installing', Uninstalling = 'Uninstalling' }
|
enum BusyState { Installing = 'Installing', Uninstalling = 'Uninstalling' }
|
||||||
|
|
||||||
const FORCE_ENABLE = ['tabby-core', 'tabby-settings']
|
const FORCE_ENABLE = ['tabby-core', 'tabby-settings', 'tabby-electron', 'tabby-web']
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -0,0 +1,116 @@
|
|||||||
|
h3.mb-3 Config sync
|
||||||
|
|
||||||
|
ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||||
|
li(ngbNavItem)
|
||||||
|
a(ngbNavLink) Sync
|
||||||
|
ng-template(ngbNavContent)
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Sync host
|
||||||
|
|
||||||
|
input.form-control(
|
||||||
|
type='text',
|
||||||
|
[(ngModel)]='config.store.configSync.host',
|
||||||
|
(ngModelChange)='config.save()',
|
||||||
|
)
|
||||||
|
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Secret sync token
|
||||||
|
.description Get it from the Tabby Web settings window
|
||||||
|
|
||||||
|
.input-group
|
||||||
|
input.form-control(
|
||||||
|
type='password',
|
||||||
|
[(ngModel)]='config.store.configSync.token',
|
||||||
|
(ngModelChange)='config.save(); testConnection()'
|
||||||
|
)
|
||||||
|
.input-group-append(*ngIf='config.store.configSync.token')
|
||||||
|
.input-group-text
|
||||||
|
i.fas.fa-fw.fa-circle-notch.fa-spin.text-warning(*ngIf='connectionSuccessful === null')
|
||||||
|
i.fas.fa-fw.fa-check.text-success(*ngIf='connectionSuccessful')
|
||||||
|
i.fas.fa-fw.fa-exclamation-triangle.text-danger(*ngIf='connectionSuccessful === false')
|
||||||
|
|
||||||
|
ng-container(*ngIf='config.store.configSync.token')
|
||||||
|
.alert.alert-danger(*ngIf='connectionSuccessful === false')
|
||||||
|
i.fas.fa-exclamation-triangle
|
||||||
|
span.ml-2 Connection failed: {{connectionError}}
|
||||||
|
|
||||||
|
ng-container(*ngIf='connectionSuccessful')
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Configs
|
||||||
|
|
||||||
|
div(*ngIf='configs === null')
|
||||||
|
i.fas.fa-fw.fa-circle-notch.fa-spin
|
||||||
|
span.ml-2 Loading configs...
|
||||||
|
|
||||||
|
ng-container(*ngIf='configs !== null')
|
||||||
|
.list-group-light
|
||||||
|
.list-group-item.d-flex.align-items-center(
|
||||||
|
*ngFor='let cfg of configs',
|
||||||
|
[class.active]='cfg.id === config.store.configSync.configID',
|
||||||
|
)
|
||||||
|
i.fas.fa-fw.fa-file
|
||||||
|
.ml-2.d-flex.flex-column.align-items-start
|
||||||
|
div {{cfg.name}}
|
||||||
|
small.text-muted Modified on {{cfg.modified_at|date:'medium'}}
|
||||||
|
.badge.badge-info(*ngIf='cfg.id === config.store.configSync.configID') ACTIVE
|
||||||
|
.mr-auto
|
||||||
|
button.btn.btn-link.ml-1(
|
||||||
|
(click)='uploadAndSync(cfg)',
|
||||||
|
[class.hover-reveal]='cfg.id !== config.store.configSync.configID'
|
||||||
|
)
|
||||||
|
i.fas.fa-arrow-up
|
||||||
|
span.ml-2 Upload
|
||||||
|
button.btn.btn-link.ml-1(
|
||||||
|
(click)='downloadAndSync(cfg)',
|
||||||
|
[class.hover-reveal]='cfg.id !== config.store.configSync.configID'
|
||||||
|
)
|
||||||
|
i.fas.fa-arrow-down
|
||||||
|
span.ml-2 Download
|
||||||
|
a.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||||
|
href='#',
|
||||||
|
(click)='uploadAsNew()'
|
||||||
|
)
|
||||||
|
i.fas.fa-fw.fa-cloud-upload-alt
|
||||||
|
.ml-2 Upload as a new config
|
||||||
|
|
||||||
|
ng-container(*ngIf='config.store.configSync.configID')
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Sync automatically
|
||||||
|
|
||||||
|
toggle(
|
||||||
|
[(ngModel)]='config.store.configSync.auto',
|
||||||
|
(ngModelChange)='config.save()',
|
||||||
|
)
|
||||||
|
|
||||||
|
li(ngbNavItem)
|
||||||
|
a(ngbNavLink) Advanced
|
||||||
|
ng-template(ngbNavContent)
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Sync hotkeys
|
||||||
|
toggle(
|
||||||
|
[(ngModel)]='config.store.configSync.parts.hotkeys',
|
||||||
|
(ngModelChange)='config.save()',
|
||||||
|
)
|
||||||
|
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Sync window settings
|
||||||
|
toggle(
|
||||||
|
[(ngModel)]='config.store.configSync.parts.appearance',
|
||||||
|
(ngModelChange)='config.save()',
|
||||||
|
)
|
||||||
|
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Sync Vault
|
||||||
|
toggle(
|
||||||
|
[(ngModel)]='config.store.configSync.parts.vault',
|
||||||
|
(ngModelChange)='config.save()',
|
||||||
|
)
|
||||||
|
|
||||||
|
div([ngbNavOutlet]='nav')
|
@ -0,0 +1,99 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { BaseComponent, ConfigService, PromptModalComponent, HostAppService, PlatformService, NotificationsService } from 'tabby-core'
|
||||||
|
import { Config, ConfigSyncService } from '../services/configSync.service'
|
||||||
|
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Component({
|
||||||
|
selector: 'config-sync-settings-tab',
|
||||||
|
template: require('./configSyncSettingsTab.component.pug'),
|
||||||
|
})
|
||||||
|
export class ConfigSyncSettingsTabComponent extends BaseComponent {
|
||||||
|
connectionSuccessful: boolean|null = null
|
||||||
|
connectionError: Error|null = null
|
||||||
|
configs: Config[]|null = null
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
public config: ConfigService,
|
||||||
|
private configSync: ConfigSyncService,
|
||||||
|
private hostApp: HostAppService,
|
||||||
|
private ngbModal: NgbModal,
|
||||||
|
private platform: PlatformService,
|
||||||
|
private notifications: NotificationsService,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit () {
|
||||||
|
await this.testConnection()
|
||||||
|
this.loadConfigs()
|
||||||
|
}
|
||||||
|
|
||||||
|
async testConnection () {
|
||||||
|
if (!this.config.store.configSync.host || !this.config.store.configSync.token) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.connectionSuccessful = null
|
||||||
|
try {
|
||||||
|
await this.configSync.getUser()
|
||||||
|
this.connectionSuccessful = true
|
||||||
|
this.loadConfigs()
|
||||||
|
} catch (e) {
|
||||||
|
this.connectionSuccessful = false
|
||||||
|
this.connectionError = e
|
||||||
|
this.configs = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadConfigs () {
|
||||||
|
this.configs = await this.configSync.getConfigs()
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadAsNew () {
|
||||||
|
let name = `New config on ${this.hostApp.platform}`
|
||||||
|
const modal = this.ngbModal.open(PromptModalComponent)
|
||||||
|
modal.componentInstance.prompt = 'Name for the new config'
|
||||||
|
modal.componentInstance.value = name
|
||||||
|
name = (await modal.result)?.value
|
||||||
|
if (!name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const cfg = await this.configSync.createNewConfig(name)
|
||||||
|
this.loadConfigs()
|
||||||
|
this.configSync.setConfig(cfg)
|
||||||
|
this.uploadAndSync(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadAndSync (cfg: Config) {
|
||||||
|
if (this.config.store.configSync.configID !== cfg.id) {
|
||||||
|
if ((await this.platform.showMessageBox({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Overwrite the config on the remote side and start syncing?',
|
||||||
|
buttons: ['Overwrite remote and sync', 'Cancel'],
|
||||||
|
defaultId: 1,
|
||||||
|
})).response === 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.configSync.setConfig(cfg)
|
||||||
|
await this.configSync.upload()
|
||||||
|
this.loadConfigs()
|
||||||
|
this.notifications.info('Config uploaded')
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadAndSync (cfg: Config) {
|
||||||
|
if ((await this.platform.showMessageBox({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Overwrite the local config and start syncing?',
|
||||||
|
buttons: ['Overwrite local and sync', 'Cancel'],
|
||||||
|
defaultId: 1,
|
||||||
|
})).response === 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.configSync.setConfig(cfg)
|
||||||
|
await this.configSync.download()
|
||||||
|
this.notifications.info('Config downloaded')
|
||||||
|
}
|
||||||
|
}
|
@ -15,15 +15,15 @@ button.btn.btn-warning.btn-block(*ngIf='config.restartRequested', '(click)'='res
|
|||||||
.text-muted {{homeBase.appVersion}}
|
.text-muted {{homeBase.appVersion}}
|
||||||
|
|
||||||
.mb-5.mt-3
|
.mb-5.mt-3
|
||||||
button.btn.btn-secondary.mr-3((click)='homeBase.openGitHub()')
|
button.btn.btn-secondary.mr-3.mb-2((click)='homeBase.openGitHub()')
|
||||||
i.fab.fa-github
|
i.fab.fa-github
|
||||||
span GitHub
|
span GitHub
|
||||||
|
|
||||||
button.btn.btn-secondary.mr-3((click)='homeBase.reportBug()')
|
button.btn.btn-secondary.mr-3.mb-2((click)='homeBase.reportBug()')
|
||||||
i.fas.fa-bug
|
i.fas.fa-bug
|
||||||
span Report a problem
|
span Report a problem
|
||||||
|
|
||||||
button.btn.btn-secondary.mr-3(
|
button.btn.btn-secondary.mr-3.mb-2(
|
||||||
(click)='showReleaseNotes()',
|
(click)='showReleaseNotes()',
|
||||||
)
|
)
|
||||||
i.fas.fa-book
|
i.fas.fa-book
|
||||||
@ -90,7 +90,7 @@ button.btn.btn-warning.btn-block(*ngIf='config.restartRequested', '(click)'='res
|
|||||||
.d-flex.flex-column.w-100.h-100
|
.d-flex.flex-column.w-100.h-100
|
||||||
.h-100.d-flex
|
.h-100.d-flex
|
||||||
.w-100.d-flex.flex-column
|
.w-100.d-flex.flex-column
|
||||||
h3 Config File
|
h3 Config file
|
||||||
textarea.form-control.h-100(
|
textarea.form-control.h-100(
|
||||||
[(ngModel)]='configFile'
|
[(ngModel)]='configFile'
|
||||||
)
|
)
|
||||||
|
@ -2,7 +2,19 @@ import { ConfigProvider, Platform } from 'tabby-core'
|
|||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
export class SettingsConfigProvider extends ConfigProvider {
|
export class SettingsConfigProvider extends ConfigProvider {
|
||||||
defaults = { }
|
defaults = {
|
||||||
|
configSync: {
|
||||||
|
host: 'https://tabby.sh',
|
||||||
|
token: '',
|
||||||
|
configID: null,
|
||||||
|
auto: false,
|
||||||
|
parts: {
|
||||||
|
hotkeys: true,
|
||||||
|
appearance: true,
|
||||||
|
vault: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
platformDefaults = {
|
platformDefaults = {
|
||||||
[Platform.macOS]: {
|
[Platform.macOS]: {
|
||||||
hotkeys: {
|
hotkeys: {
|
||||||
|
@ -17,12 +17,15 @@ import { VaultSettingsTabComponent } from './components/vaultSettingsTab.compon
|
|||||||
import { SetVaultPassphraseModalComponent } from './components/setVaultPassphraseModal.component'
|
import { SetVaultPassphraseModalComponent } from './components/setVaultPassphraseModal.component'
|
||||||
import { ProfilesSettingsTabComponent } from './components/profilesSettingsTab.component'
|
import { ProfilesSettingsTabComponent } from './components/profilesSettingsTab.component'
|
||||||
import { ReleaseNotesComponent } from './components/releaseNotesTab.component'
|
import { ReleaseNotesComponent } from './components/releaseNotesTab.component'
|
||||||
|
import { ConfigSyncSettingsTabComponent } from './components/configSyncSettingsTab.component'
|
||||||
|
|
||||||
|
import { ConfigSyncService } from './services/configSync.service'
|
||||||
|
|
||||||
import { SettingsTabProvider } from './api'
|
import { SettingsTabProvider } from './api'
|
||||||
import { ButtonProvider } from './buttonProvider'
|
import { ButtonProvider } from './buttonProvider'
|
||||||
import { SettingsHotkeyProvider } from './hotkeys'
|
import { SettingsHotkeyProvider } from './hotkeys'
|
||||||
import { SettingsConfigProvider } from './config'
|
import { SettingsConfigProvider } from './config'
|
||||||
import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabProvider, ProfilesSettingsTabProvider } from './settings'
|
import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabProvider, ProfilesSettingsTabProvider, ConfigSyncSettingsTabProvider } from './settings'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -41,6 +44,7 @@ import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabP
|
|||||||
{ provide: SettingsTabProvider, useClass: WindowSettingsTabProvider, multi: true },
|
{ provide: SettingsTabProvider, useClass: WindowSettingsTabProvider, multi: true },
|
||||||
{ provide: SettingsTabProvider, useClass: VaultSettingsTabProvider, multi: true },
|
{ provide: SettingsTabProvider, useClass: VaultSettingsTabProvider, multi: true },
|
||||||
{ provide: SettingsTabProvider, useClass: ProfilesSettingsTabProvider, multi: true },
|
{ provide: SettingsTabProvider, useClass: ProfilesSettingsTabProvider, multi: true },
|
||||||
|
{ provide: SettingsTabProvider, useClass: ConfigSyncSettingsTabProvider, multi: true },
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
EditProfileModalComponent,
|
EditProfileModalComponent,
|
||||||
@ -51,6 +55,7 @@ import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabP
|
|||||||
SetVaultPassphraseModalComponent,
|
SetVaultPassphraseModalComponent,
|
||||||
VaultSettingsTabComponent,
|
VaultSettingsTabComponent,
|
||||||
WindowSettingsTabComponent,
|
WindowSettingsTabComponent,
|
||||||
|
ConfigSyncSettingsTabComponent,
|
||||||
ReleaseNotesComponent,
|
ReleaseNotesComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -64,10 +69,13 @@ import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabP
|
|||||||
SetVaultPassphraseModalComponent,
|
SetVaultPassphraseModalComponent,
|
||||||
VaultSettingsTabComponent,
|
VaultSettingsTabComponent,
|
||||||
WindowSettingsTabComponent,
|
WindowSettingsTabComponent,
|
||||||
|
ConfigSyncSettingsTabComponent,
|
||||||
ReleaseNotesComponent,
|
ReleaseNotesComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class SettingsModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class
|
export default class SettingsModule {
|
||||||
|
constructor (public configSync: ConfigSyncService) { }
|
||||||
|
}
|
||||||
|
|
||||||
export * from './api'
|
export * from './api'
|
||||||
export { SettingsTabComponent }
|
export { SettingsTabComponent }
|
||||||
|
179
tabby-settings/src/services/configSync.service.ts
Normal file
179
tabby-settings/src/services/configSync.service.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import * as yaml from 'js-yaml'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { ConfigService, HostAppService, Logger, LogService, Platform, PlatformService } from 'tabby-core'
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
content: string
|
||||||
|
last_used_with_version: string|null
|
||||||
|
created_at: Date
|
||||||
|
modified_at: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPTIONAL_CONFIG_PARTS = ['hotkeys', 'appearance', 'vault']
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class ConfigSyncService {
|
||||||
|
private logger: Logger
|
||||||
|
private lastRemoteChange = new Date(0)
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
log: LogService,
|
||||||
|
private platform: PlatformService,
|
||||||
|
private hostApp: HostAppService,
|
||||||
|
private config: ConfigService,
|
||||||
|
) {
|
||||||
|
this.logger = log.create('configSync')
|
||||||
|
config.ready$.toPromise().then(() => {
|
||||||
|
this.autoSync()
|
||||||
|
config.changed$.subscribe(() => {
|
||||||
|
if (this.isEnabled() && this.config.store.configSync.auto) {
|
||||||
|
this.upload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
isAvailable (): boolean {
|
||||||
|
return this.config.store.enableExperimentalFeatures && this.hostApp.platform !== Platform.Web
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled (): boolean {
|
||||||
|
return this.isAvailable() &&
|
||||||
|
!!this.config.store.configSync.host &&
|
||||||
|
!!this.config.store.configSync.token &&
|
||||||
|
!!this.config.store.configSync.configID
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfigs (): Promise<Config[]> {
|
||||||
|
return this.request('GET', '/api/1/configs')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfig (id: number): Promise<Config> {
|
||||||
|
return this.request('GET', `/api/1/configs/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateConfig (id: number, data: Partial<Config>): Promise<Config> {
|
||||||
|
return this.request('PATCH', `/api/1/configs/${id}`, { data })
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser (): Promise<any> {
|
||||||
|
return this.request('GET', '/api/1/user')
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNewConfig (name: string): Promise<Config> {
|
||||||
|
return this.request('POST', '/api/1/configs', {
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig (config: Config): void {
|
||||||
|
this.config.store.configSync.configID = config.id
|
||||||
|
this.config.save()
|
||||||
|
this.lastRemoteChange = new Date(config.modified_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
async upload (): Promise<void> {
|
||||||
|
if (!this.isEnabled()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = this.readConfigDataForSync()
|
||||||
|
const remoteData = yaml.load((await this.getConfig(this.config.store.configSync.configID)).content) as any
|
||||||
|
for (const part of OPTIONAL_CONFIG_PARTS) {
|
||||||
|
if (!this.config.store.configSync.parts[part]) {
|
||||||
|
data[part] = remoteData[part]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const content = yaml.dump(data)
|
||||||
|
const result = await this.updateConfig(this.config.store.configSync.configID, {
|
||||||
|
content,
|
||||||
|
last_used_with_version: this.platform.getAppVersion(),
|
||||||
|
})
|
||||||
|
this.lastRemoteChange = new Date(result.modified_at)
|
||||||
|
this.logger.debug('Config uploaded')
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Upload failed:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async download (): Promise<void> {
|
||||||
|
if (!this.isEnabled()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const config = await this.getConfig(this.config.store.configSync.configID)
|
||||||
|
const data = yaml.load(config.content) as any
|
||||||
|
const localData = yaml.load(this.config.readRaw()) as any
|
||||||
|
data.configSync = localData.configSync
|
||||||
|
|
||||||
|
for (const part of OPTIONAL_CONFIG_PARTS) {
|
||||||
|
if (!this.config.store.configSync.parts[part]) {
|
||||||
|
data[part] = localData[part]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.writeConfigDataFromSync(data)
|
||||||
|
this.logger.debug('Config downloaded')
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Download failed:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readConfigDataForSync (): any {
|
||||||
|
const data = yaml.load(this.config.readRaw()) as any
|
||||||
|
delete data.configSync
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeConfigDataFromSync (data: any) {
|
||||||
|
this.config.writeRaw(yaml.dump(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request (method: 'GET'|'POST'|'PATCH', url: string, params = {}) {
|
||||||
|
if (this.config.store.configSync.host.endsWith('/')) {
|
||||||
|
this.config.store.configSync.host = this.config.store.configSync.host.slice(0, -1)
|
||||||
|
}
|
||||||
|
url = this.config.store.configSync.host + url
|
||||||
|
this.logger.debug(`${method} ${url}`, params)
|
||||||
|
try {
|
||||||
|
const response = await axios.request({
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.config.store.configSync.token}`,
|
||||||
|
},
|
||||||
|
...params,
|
||||||
|
})
|
||||||
|
this.logger.debug(response)
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async autoSync () {
|
||||||
|
while (true) {
|
||||||
|
if (this.isEnabled() && this.config.store.configSync.auto) {
|
||||||
|
const cfg = await this.getConfig(this.config.store.configSync.configID)
|
||||||
|
if (new Date(cfg.modified_at) > this.lastRemoteChange) {
|
||||||
|
this.logger.info('Remote config changed, downloading')
|
||||||
|
this.download()
|
||||||
|
this.lastRemoteChange = new Date(cfg.modified_at)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,9 @@ import { SettingsTabProvider } from './api'
|
|||||||
import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component'
|
import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component'
|
||||||
import { WindowSettingsTabComponent } from './components/windowSettingsTab.component'
|
import { WindowSettingsTabComponent } from './components/windowSettingsTab.component'
|
||||||
import { VaultSettingsTabComponent } from './components/vaultSettingsTab.component'
|
import { VaultSettingsTabComponent } from './components/vaultSettingsTab.component'
|
||||||
|
import { ConfigSyncSettingsTabComponent } from './components/configSyncSettingsTab.component'
|
||||||
import { ProfilesSettingsTabComponent } from './components/profilesSettingsTab.component'
|
import { ProfilesSettingsTabComponent } from './components/profilesSettingsTab.component'
|
||||||
|
import { ConfigSyncService } from './services/configSync.service'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -55,3 +57,22 @@ export class ProfilesSettingsTabProvider extends SettingsTabProvider {
|
|||||||
return ProfilesSettingsTabComponent
|
return ProfilesSettingsTabComponent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Injectable()
|
||||||
|
export class ConfigSyncSettingsTabProvider extends SettingsTabProvider {
|
||||||
|
id = 'config-sync'
|
||||||
|
icon = 'cloud'
|
||||||
|
title = 'Config sync'
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private configSync: ConfigSyncService,
|
||||||
|
) { super() }
|
||||||
|
|
||||||
|
getComponentType (): any {
|
||||||
|
if (!this.configSync.isAvailable()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return ConfigSyncSettingsTabComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -22,7 +22,7 @@ export class AppearanceSettingsTabProvider extends SettingsTabProvider {
|
|||||||
export class ColorSchemeSettingsTabProvider extends SettingsTabProvider {
|
export class ColorSchemeSettingsTabProvider extends SettingsTabProvider {
|
||||||
id = 'terminal-color-scheme'
|
id = 'terminal-color-scheme'
|
||||||
icon = 'palette'
|
icon = 'palette'
|
||||||
title = 'Color Scheme'
|
title = 'Color scheme'
|
||||||
|
|
||||||
getComponentType (): any {
|
getComponentType (): any {
|
||||||
return ColorSchemeSettingsTabComponent
|
return ColorSchemeSettingsTabComponent
|
||||||
|
Loading…
x
Reference in New Issue
Block a user