This commit is contained in:
Eugene Pankov
2017-04-11 22:45:59 +02:00
parent 0ea346a6ae
commit dc513b427d
114 changed files with 454 additions and 374 deletions

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core'
export declare type ComponentType = new (...args: any[]) => Component
export abstract class SettingsTabProvider {
title: string
getComponentType (): ComponentType {
return null
}
}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core'
import { ToolbarButtonProvider, IToolbarButton, AppService } from 'terminus-core'
import { SettingsTabComponent } from './components/settingsTab'
@Injectable()
export class ButtonProvider extends ToolbarButtonProvider {
constructor (
private app: AppService,
) {
super()
}
provide (): IToolbarButton[] {
return [{
icon: 'cog',
title: 'Settings',
weight: 10,
click: () => {
let settingsTab = this.app.tabs.find((tab) => tab instanceof SettingsTabComponent)
if (settingsTab) {
this.app.selectTab(settingsTab)
} else {
this.app.openNewTab(SettingsTabComponent)
}
}
}]
}
}

View File

@@ -0,0 +1,7 @@
.stroke(*ngFor='let stroke of model')
.key-container(
*ngFor='let key of splitKeys(stroke); let isLast = last; trackBy: key',
[@animateKey]='animate ? "in" : ""'
)
.key {{key}}
.plus(*ngIf='!isLast') +

View File

@@ -0,0 +1,26 @@
:host {
display: inline-block;
.stroke {
display: inline-block;
margin: 0 5px;
background: #222;
border-radius: 2px;
box-shadow: 0 1px 0 rgba(0,0,0,.5);
text-shadow: 0 1px 0 rgba(0,0,0,.5);
.key-container {
display: inline-block;
.key {
display: inline-block;
padding: 4px 5px;
}
.plus {
display: inline-block;
padding: 4px 2px;
}
}
}
}

View File

@@ -0,0 +1,41 @@
import { Component, Input, trigger, style, animate, transition } from '@angular/core'
@Component({
selector: 'hotkey-display',
template: require('./hotkeyDisplay.pug'),
styles: [require('./hotkeyDisplay.scss')],
//changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('animateKey', [
transition('void => in', [
style({
transform: 'translateX(25px)',
opacity: '0',
}),
animate('250ms ease-out', style({
transform: 'translateX(0)',
opacity: '1',
}))
]),
transition('in => void', [
style({
transform: 'translateX(0)',
opacity: '1',
}),
animate('250ms ease-in', style({
transform: 'translateX(25px)',
opacity: '0',
}))
])
])
]
})
export class HotkeyDisplayComponent {
splitKeys(keys: string): string[] {
return keys.split('+').map((x) => x.trim())
}
@Input() model: string[]
@Input() animate = false
}

View File

@@ -0,0 +1,14 @@
:host {
display: inline-block;
padding: 5px;
transition: 0.125s all;
&:hover:not(.active) {
background: rgba(255, 255, 255, .033);
}
&:active:not(.active) {
background: rgba(0, 0, 0, .1);
}
}

View File

@@ -0,0 +1,33 @@
import { Component, Input, Output, EventEmitter, HostListener, ChangeDetectionStrategy } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { HotkeyInputModalComponent } from './hotkeyInputModal'
@Component({
selector: 'hotkey-input',
template: `
<hotkey-display [model]='model'></hotkey-display>
`,
styles: [require('./hotkeyInput.scss')],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HotkeyInputComponent {
constructor(
private ngbModal: NgbModal,
) { }
@HostListener('click') public click() {
this.ngbModal.open(HotkeyInputModalComponent).result.then((value: string[]) => {
this.model = value
this.modelChange.emit(this.model)
})
}
splitKeys(keys: string): string[] {
return keys.split('+').map((x) => x.trim())
}
@Input() model: string[]
@Output() modelChange = new EventEmitter()
}

View File

@@ -0,0 +1,12 @@
.modal-header
h5 Press the key now
.modal-body
.input
.stroke(*ngFor='let stroke of value', [@animateKey]='true') {{stroke}}
.timeout
div([style.width]='timeoutProgress + "%"')
.modal-footer
button.btn.btn-outline-primary((click)='close()') Cancel

View File

@@ -0,0 +1,22 @@
:host {
>.modal-body {
padding: 0 !important;
}
.input {
display: flex;
.stroke {
flex: none;
}
}
.timeout {
height: 5px;
margin: 0 0 15px;
div {
height: 5px;
}
}
}

View File

@@ -0,0 +1,84 @@
import { Component, Input, trigger, transition, style, animate } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { Subscription } from 'rxjs'
import { HotkeysService } from 'terminus-core'
const INPUT_TIMEOUT = 1000
@Component({
selector: 'hotkey-input-modal',
template: require('./hotkeyInputModal.pug'),
styles: [require('./hotkeyInputModal.scss')],
animations: [
trigger('animateKey', [
transition(':enter', [
style({
transform: 'translateX(25px)',
opacity: '0',
}),
animate('250ms ease-out', style({
transform: 'translateX(0)',
opacity: '1',
}))
]),
transition(':leave', [
style({
transform: 'translateX(0)',
opacity: '1',
}),
animate('250ms ease-in', style({
transform: 'translateX(25px)',
opacity: '0',
}))
])
])
]
})
export class HotkeyInputModalComponent {
private keySubscription: Subscription
private lastKeyEvent: number
private keyTimeoutInterval: number = null
@Input() value: string[] = []
@Input() timeoutProgress = 0
constructor(
private modalInstance: NgbActiveModal,
public hotkeys: HotkeysService,
) {
this.hotkeys.clearCurrentKeystrokes()
this.keySubscription = hotkeys.key.subscribe((event) => {
this.lastKeyEvent = performance.now()
this.value = this.hotkeys.getCurrentKeystrokes()
event.preventDefault()
event.stopPropagation()
})
}
splitKeys (keys: string): string[] {
return keys.split('+').map((x) => x.trim())
}
ngOnInit () {
this.keyTimeoutInterval = window.setInterval(() => {
if (!this.lastKeyEvent) {
return
}
this.timeoutProgress = Math.min(100, (performance.now() - this.lastKeyEvent) * 100 / INPUT_TIMEOUT)
if (this.timeoutProgress == 100) {
this.modalInstance.close(this.value)
}
}, 25)
this.hotkeys.disable()
}
ngOnDestroy () {
this.hotkeys.enable()
clearInterval(this.keyTimeoutInterval)
}
close() {
this.modalInstance.dismiss()
}
}

View File

@@ -0,0 +1,6 @@
.item(*ngFor='let item of model')
.body((click)='editItem(item)')
.stroke(*ngFor='let stroke of item') {{stroke}}
.remove((click)='removeItem(item)') &times;
.add((click)='addItem()') Add...

View File

@@ -0,0 +1,25 @@
:host {
display: flex;
}
.item {
display: flex;
flex: none;
.body {
flex: none;
display: flex;
.stroke {
flex: none;
}
}
.remove {
flex: none;
}
}
.add {
flex: auto;
}

View File

@@ -0,0 +1,49 @@
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { HotkeyInputModalComponent } from './hotkeyInputModal'
@Component({
selector: 'multi-hotkey-input',
template: require('./multiHotkeyInput.pug'),
styles: [require('./multiHotkeyInput.scss')],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MultiHotkeyInputComponent {
constructor (
private ngbModal: NgbModal,
) { }
ngOnInit () {
if (!this.model) {
this.model = []
}
if (typeof this.model == 'string') {
this.model = [this.model]
}
this.model = this.model.map(item => (typeof item == 'string') ? [item] : item)
}
editItem (item) {
this.ngbModal.open(HotkeyInputModalComponent).result.then((value: string[]) => {
this.model[this.model.findIndex(x => x === item)] = value
this.model = this.model.slice()
this.modelChange.emit(this.model)
})
}
addItem () {
this.ngbModal.open(HotkeyInputModalComponent).result.then((value: string[]) => {
this.model = this.model.concat([value])
this.modelChange.emit(this.model)
})
}
removeItem (item) {
this.model = this.model.filter(x => x !== item)
this.modelChange.emit(this.model)
}
@Input() model: string[][]
@Output() modelChange = new EventEmitter()
}

View File

@@ -0,0 +1,17 @@
:host /deep/ ngb-tabset {
flex: auto;
display: flex;
}
:host /deep/ ngb-tabset > .nav {
display: flex;
flex-direction: column;
flex: none;
}
:host /deep/ ngb-tabset > .tab-content {
padding: 15px 30px;
margin: 0;
flex: auto;
overflow-y: auto;
}

View File

@@ -0,0 +1,146 @@
button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(click)'='restartApp()') Restart the app to apply changes
ngb-tabset.vertical(type='tabs')
ngb-tab(*ngFor='let provider of settingsProviders')
template(ngbTabTitle)
| {{provider.title}}
template(ngbTabContent)
settings-tab-body([provider]='provider')
ngb-tab
template(ngbTabTitle)
| Application
template(ngbTabContent)
.row
.col.col-lg-6
.form-group
label Show tabs
br
div(
'[(ngModel)]'='config.store.appearance.tabsOnTop'
'(ngModelChange)'='config.save()'
ngbRadioGroup
)
label.btn.btn-secondary
input(
type='radio',
[value]='true'
)
| On the top
label.btn.btn-secondary
input(
type='radio',
[value]='false'
)
| At the bottom
.col.col-lg-6
.form-group
label Window frame
br
div(
'[(ngModel)]'='config.store.appearance.useNativeFrame'
'(ngModelChange)'='config.save(); config.requestRestart()'
ngbRadioGroup
)
label.btn.btn-secondary
input(
type='radio',
[value]='true'
)
| Native
label.btn.btn-secondary
input(
type='radio',
[value]='false'
)
| Custom
small.form-text.text-muted Whether a custom window or an OS native window should be used
.row
.col.col-auto
.form-group
label Dock the terminal
br
div(
'[(ngModel)]'='config.store.appearance.dock'
'(ngModelChange)'='config.save(); docking.dock()'
ngbRadioGroup
)
label.btn.btn-secondary
input(
type='radio',
[value]='"off"'
)
| Off
label.btn.btn-secondary
input(
type='radio',
[value]='"top"'
)
| Top
label.btn.btn-secondary
input(
type='radio',
[value]='"left"'
)
| Left
label.btn.btn-secondary
input(
type='radio',
[value]='"right"'
)
| Right
label.btn.btn-secondary
input(
type='radio',
[value]='"bottom"'
)
| Bottom
.form-group(*ngIf='config.store.appearance.dock != "off"')
label Display on
br
div(
'[(ngModel)]'='config.store.appearance.dockScreen'
'(ngModelChange)'='config.save(); docking.dock()'
ngbRadioGroup
)
label.btn.btn-secondary
input(
type='radio',
[value]='"current"'
)
| Current
label.btn.btn-secondary(*ngFor='let screen of docking.getScreens()')
input(
type='radio',
[value]='screen.id'
)
| {{screen.name}}
.col.col-auto
.form-group(*ngIf='config.store.appearance.dock != "off"')
label Docked terminal size
br
input(
type='range',
'[(ngModel)]'='config.store.appearance.dockFill',
'(mouseup)'='config.save(); docking.dock()',
min='0.05',
max='1',
step='0.01'
)
ngb-tab
template(ngbTabTitle)
| Hotkeys
template(ngbTabContent)
.form-group
table
tr
th Toggle terminal window
td
hotkey-input('[(model)]'='globalHotkey')
tr(*ngFor='let hotkey of hotkeyDescriptions')
th {{hotkey.name}}
td
multi-hotkey-input('[(model)]'='config.store.hotkeys[hotkey.id]')

View File

@@ -0,0 +1,15 @@
:host {
display: flex;
flex-direction: column;
flex: auto;
>.btn-block {
margin: 20px;
width: auto;
flex: none;
}
>.modal-body {
padding: 0 0 20px !important;
}
}

View File

@@ -0,0 +1,44 @@
import { Component, Inject } from '@angular/core'
import { ElectronService, DockingService, ConfigService, IHotkeyDescription, HotkeyProvider, BaseTabComponent } from 'terminus-core'
import { SettingsTabProvider } from '../api'
@Component({
selector: 'settings-tab',
template: require('./settingsTab.pug'),
styles: [
require('./settingsTab.scss'),
require('./settingsTab.deep.css'),
],
})
export class SettingsTabComponent extends BaseTabComponent {
globalHotkey = ['Ctrl+Shift+G']
private hotkeyDescriptions: IHotkeyDescription[]
constructor(
public config: ConfigService,
private electron: ElectronService,
public docking: DockingService,
@Inject(HotkeyProvider) hotkeyProviders: HotkeyProvider[],
@Inject(SettingsTabProvider) public settingsProviders: SettingsTabProvider[]
) {
super()
this.hotkeyDescriptions = hotkeyProviders.map(x => x.hotkeys).reduce((a, b) => a.concat(b))
this.title$.next('Settings')
this.scrollable = true
}
getRecoveryToken (): any {
return { type: 'app:settings' }
}
ngOnDestroy () {
this.config.save()
}
restartApp () {
this.electron.app.relaunch()
this.electron.app.exit()
}
}

View File

@@ -0,0 +1,26 @@
import { Component, Input, ViewContainerRef, ViewChild, ComponentFactoryResolver, ComponentRef } from '@angular/core'
import { SettingsTabProvider } from '../api'
@Component({
selector: 'settings-tab-body',
template: '<ng-template #placeholder></ng-template>',
})
export class SettingsTabBodyComponent {
@Input() provider: SettingsTabProvider
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef
private component: ComponentRef<Component>
constructor (private componentFactoryResolver: ComponentFactoryResolver) { }
ngAfterViewInit () {
// run after the change detection finishes
setImmediate(() => {
this.component = this.placeholder.createComponent(
this.componentFactoryResolver.resolveComponentFactory(
this.provider.getComponentType()
)
)
})
}
}

View File

@@ -0,0 +1,44 @@
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ToolbarButtonProvider, TabRecoveryProvider } from 'terminus-core'
import { HotkeyInputComponent } from './components/hotkeyInput'
import { HotkeyDisplayComponent } from './components/hotkeyDisplay'
import { HotkeyInputModalComponent } from './components/hotkeyInputModal'
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput'
import { SettingsTabComponent } from './components/settingsTab'
import { SettingsTabBodyComponent } from './components/settingsTabBody'
import { ButtonProvider } from './buttonProvider'
import { RecoveryProvider } from './recoveryProvider'
@NgModule({
imports: [
BrowserModule,
FormsModule,
NgbModule,
],
providers: [
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true }
],
entryComponents: [
HotkeyInputModalComponent,
SettingsTabComponent,
],
declarations: [
HotkeyDisplayComponent,
HotkeyInputComponent,
HotkeyInputModalComponent,
MultiHotkeyInputComponent,
SettingsTabComponent,
SettingsTabBodyComponent,
],
})
export default class SettingsModule {
}
export * from './api'

View File

@@ -0,0 +1,20 @@
import { Injectable } from '@angular/core'
import { TabRecoveryProvider, AppService } from 'terminus-core'
import { SettingsTabComponent } from './components/settingsTab'
@Injectable()
export class RecoveryProvider extends TabRecoveryProvider {
constructor(
private app: AppService
) {
super()
}
async recover (recoveryToken: any): Promise<void> {
if (recoveryToken.type == 'app:settings') {
this.app.openNewTab(SettingsTabComponent)
}
}
}