This commit is contained in:
May B. 2019-05-21 16:13:13 +02:00
parent 74935ced6c
commit ab895a7d02
25 changed files with 371 additions and 280 deletions

View File

@ -31,75 +31,35 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from 'vue-property-decorator' import { Component, Vue } from 'vue-property-decorator'
import { Selectable } from './helpers/lists/Selectable' import { Selectable } from './helpers/lists/Selectable'
import LocalStorageHandler from './helpers/storage/LocalStorageHandler'
import Discord from './services/discord/Discord.vue' import Discord from './services/discord/Discord.vue'
import Mastodon from './services/mastodon/Mastodon.vue' import Mastodon from './services/mastodon/Mastodon.vue'
import NextcloudNews from './services/nextcloud/NextcloudNews.vue' import NextcloudNews from './services/nextcloud/NextcloudNews.vue'
import OpenWeatherMap from './services/openweathermap/OpenWeatherMap.vue' import OpenWeatherMap from './services/openweathermap/OpenWeatherMap.vue'
import { ErrorsModule, ServicesModule, LayoutsModule } from './store'
import { Auth, Layout, Rect, Service, serviceKey, tileKey } from './types/App' import { Auth, Layout, Rect, Service, serviceKey, tileKey } from './types/App'
import * as Events from './types/Events'
const layoutsStorage = 'layouts'
const servicesStorage = 'services'
function saveAuth(auth: Auth) {
const res: any = {}
for (const entry of auth.entries()) {
res[entry[0]] = entry[1]
}
return res
}
@Component({ components: { Mastodon, NextcloudNews, openweathermap: OpenWeatherMap, Discord } }) @Component({ components: { Mastodon, NextcloudNews, openweathermap: OpenWeatherMap, Discord } })
export default class App extends Vue { export default class App extends Vue {
showManager = false showManager = false
layouts = new LocalStorageHandler(layoutsStorage,
new Selectable<Layout>([{ name: 'main', tiles: [] }]),
data => new Selectable<Layout>(data), l => l.data)
services = new LocalStorageHandler<Service[]>(servicesStorage, [],
(data: any[]) => data.map(s => ({ ...s, auth: new Auth(Object.entries(s.auth)) })),
data => data.map(s => ({ ...s, auth: saveAuth(s.auth) })))
newService = '' newService = ''
errors: string[] = []
bus = new Vue()
get managerButton() { get managerButton() {
return this.showManager ? '▼' : '▲' return this.showManager ? '▼' : '▲'
} }
get tiles() { get tiles() {
const layout = this.layouts.get().selected return LayoutsModule.tiles.map((tile, key: tileKey) => {
if(layout) { const service = ServicesModule.get(tile.service)
return layout.tiles.map((tile, key: tileKey) => { if(service) {
const service = this.loadService(key, tile.service) return {
if(service) { ...tile, service, serviceId: tile.service,
return { grid: this.gridPos(tile.position)
...tile, service, serviceId: tile.service,
grid: this.gridPos(tile.position), emiter: this.makeEmiter(key)
}
} }
}) } else {
} else { LayoutsModule.removeTile(key)
return [] }
} })
}
mounted() {
this.layouts.load()
this.services.load()
new Map<string, (event: Events.Message) => void>([
[ Events.ErrorEvent, this.onError ],
[ Events.SaveOptionsEvent, this.onSaveOptions ],
[ Events.SaveOptionEvent, this.onSaveOption ],
[ Events.MoveTileEvent, this.onMoveTile ],
[ Events.RemoveTileEvent, this.onRemoveTile ],
[ Events.SaveServiceEvent, this.onSaveService ],
[ Events.RemoveServiceEvent, this.onRemoveService ]
]).forEach((handler, name) => this.bus.$on(name, handler))
} }
// UI // UI
@ -107,117 +67,44 @@ export default class App extends Vue {
this.showManager = !this.showManager this.showManager = !this.showManager
} }
selectLayout(id: number) { selectLayout(id: number) {
this.layouts.get().select(id) LayoutsModule.select(id)
} }
// Layouts // Layouts
addLayout() { addLayout() {
this.layouts.edit(l => { LayoutsModule.add({ name: 'new layout', tiles: [] })
l.data.push({ name: 'layout' + l.data.length, tiles: [] })
return l
})
} }
renameSelectedLayout(name: string) { renameSelectedLayout(name: string) {
this.layouts.edit(data => { LayoutsModule.setName(name)
if(data.selected) {
data.selected.name = name
}
return data
})
} }
removeSelectedLayout() { removeSelectedLayout() {
this.layouts.edit(data => data.remove()) LayoutsModule.remove()
} }
// Tiles // Tiles
showService(id: serviceKey) { showService(id: serviceKey) {
this.layouts.edit(data => { LayoutsModule.addTile({
if(data.selected) { service: id, position: {}, options: {}
data.selected.tiles.push({
service: id, position: {}, options: {}
})
}
return data
})
}
onSaveOption({ key, msg }: Events.SaveOptionMessage) {
this.layouts.edit(data => {
if(data.selected) {
this.$set(data.selected.tiles[key].options, msg.key, msg.value)
}
return data
})
}
onSaveOptions({ key, msg }: Events.SaveOptionsMessage) {
this.layouts.edit(data => {
if(data.selected) {
let options = data.selected.tiles[key].options
options = Object.assign({}, options, msg)
}
return data
})
}
onMoveTile({ key, msg }: Events.MoveTileMessage) {
this.layouts.edit(data => {
if(data.selected){
const position = data.selected.tiles[key].position
this.$set(position, msg.type, Math.max(1,
(position[msg.type] || 1) + msg.direction
))
}
return data
})
}
onRemoveTile({ key }: Events.RemoveTileMessage) {
this.layouts.edit(data => {
if(data.selected) {
data.selected.tiles.splice(key, 1)
}
return data
}) })
} }
// Services // Services
getServiceId(key: number) {
const tile = this.tiles[key]
if(tile) {
return tile.serviceId
} else {
throw new Error('tile not found')
}
}
addService() { addService() {
if (this.newService) { if (this.newService) {
this.services.edit(data => { const id = ServicesModule.add({
data.push({ type: this.newService, name: this.newService, auth: new Auth() }) type: this.newService, name: this.newService, auth: new Auth()
return data
}) })
this.showService(this.services.get().length - 1) this.showService(id)
this.newService = '' this.newService = ''
} }
} }
onSaveService({ key, msg }: Events.SaveServiceMessage) {
const service = this.loadService(key, this.getServiceId(key))
if(service){
service.name = msg.name
service.auth = msg.auth
this.services.save()
}
}
onRemoveService({ key }: Events.RemoveServiceMessage) {
this.services.edit(data => {
data.splice(this.getServiceId(key), 1)
return data
})
this.onRemoveTile({ key, msg: undefined })
}
// Errors // Errors
onError({ msg }: Events.ErrorMessage) { get errors() {
this.errors.push(msg.toString()) return ErrorsModule.errors
} }
removeError(id: number) { removeError(id: number) {
this.errors.splice(id, 1) ErrorsModule.remove(id)
} }
// Helpers // Helpers
@ -227,21 +114,6 @@ export default class App extends Vue {
'grid-column': `${position.y || 1} / span ${position.w || 2}` 'grid-column': `${position.y || 1} / span ${position.w || 2}`
} }
} }
private makeEmiter(key: tileKey) {
const bus = this.bus
return (name: string, msg: any) => {
bus.$emit(name, { key, msg })
}
}
private loadService(key: tileKey, id: serviceKey) {
const ser = this.services.get()[id]
if (ser){
return ser
} else {
this.onRemoveTile({ key, msg: undefined })
this.errors.push('Removing missing service')
}
}
} }
</script> </script>

View File

@ -5,37 +5,37 @@ import LoadableBlock from './loadable/LoadableBlock.vue'
import LoadableInline from './loadable/LoadableInline.vue' import LoadableInline from './loadable/LoadableInline.vue'
import SuccessLoadable from './loadable/SuccessLoadableBlock.vue' import SuccessLoadable from './loadable/SuccessLoadableBlock.vue'
import { ServiceData } from '@/types/App' import { ErrorsModule, LayoutsModule, ServicesModule } from '@/store'
import * as Events from '@/types/Events' import { Options, ServiceData } from '@/types/App'
@Component({ components: { LoadableBlock, LoadableInline, SuccessLoadable } }) @Component({ components: { LoadableBlock, LoadableInline, SuccessLoadable } })
export default class ServiceEmiter extends Vue { export default class ServiceEmiter extends Vue {
@Prop(Function) @Prop(Number)
readonly emit!: (name: string, msg: any) => void readonly tileKey!: number
emitError(err: string) { addError(error: any) {
this.emit(Events.ErrorEvent, err) ErrorsModule.add(error.toString())
} }
saveOptions(options: object) { saveOptions(options: Options) {
this.emit(Events.SaveOptionsEvent, options) LayoutsModule.setTileOptions(this.tileKey, options)
} }
saveOption(key: string, value: any) { saveOption(key: string, value: any) {
this.saveOptionCouple({ key, value }) LayoutsModule.setTileOption(this.tileKey, key, value)
} }
saveOptionCouple(couple: Events.Option) { saveOptionCouple({ key, value }: { key: string, value: any }) {
this.emit(Events.SaveOptionEvent, couple) this.saveOption(key, value)
} }
saveService(service: ServiceData) { saveService({ name, auth }: ServiceData) {
this.emit(Events.SaveServiceEvent, service) ServicesModule.set(LayoutsModule.getTile(this.tileKey).service, name, auth)
} }
catchEmit(req: AxiosPromise) { catchError(req: AxiosPromise) {
return req.catch(err => { return req.catch(err => {
this.emitError(err) this.addError(err)
throw err throw err
}) })
} }

View File

@ -16,24 +16,25 @@
<script lang="ts"> <script lang="ts">
import { Component, Mixins } from 'vue-property-decorator' import { Component, Mixins } from 'vue-property-decorator'
import * as Events from '../types/Events'
import ServiceEmiter from './ServiceEmiter' import ServiceEmiter from './ServiceEmiter'
import { MoveType, MoveDirection } from '@/types/App';
import { LayoutsModule, ServicesModule } from '../store';
@Component @Component
export default class ServiceHeader extends Mixins(ServiceEmiter) { export default class ServiceHeader extends Mixins(ServiceEmiter) {
showSettings = false showSettings = false
onMove(type: Events.MoveType, direction: Events.MoveDirection) { onMove(type: MoveType, direction: MoveDirection) {
this.emit(Events.MoveTileEvent, { type, direction }) LayoutsModule.moveTile(this.tileKey, type, direction)
} }
onRemove() { onRemove() {
this.emit(Events.RemoveTileEvent, {}) LayoutsModule.removeTile(this.tileKey)
} }
onRemoveService() { onRemoveService() {
this.emit(Events.RemoveServiceEvent, {}) ServicesModule.remove(LayoutsModule.getTile(this.tileKey).service)
} }
} }

View File

@ -2,7 +2,7 @@ import { AxiosPromise, AxiosResponse } from 'axios'
import { Component } from 'vue-property-decorator' import { Component } from 'vue-property-decorator'
import AxiosLoadable from '@/helpers/loadable/AxiosLoadable' import AxiosLoadable from '@/helpers/loadable/AxiosLoadable'
import { unsafeAxiosMapper } from '@/helpers/unsafeAxiosMapper' import { unsafeAxiosMapper } from '@/helpers/unsafeMapper'
import { Auth } from '@/types/App' import { Auth } from '@/types/App'
import ConnectedService from './ConnectedService' import ConnectedService from './ConnectedService'
@ -16,7 +16,7 @@ export default class AccountService<T, E, U = T> extends ConnectedService<T, E>
} }
load() { load() {
this.account.load(this.catchEmit(this.getAccount(this.auth)), this.mapAccount) this.account.load(this.catchError(this.getAccount(this.auth)), this.mapAccount)
} }
mapAccount(res: AxiosResponse<U>) { mapAccount(res: AxiosResponse<U>) {
return unsafeAxiosMapper<T, U>(res) return unsafeAxiosMapper<T, U>(res)

View File

@ -30,7 +30,7 @@ export default class ConnectedService<T, E> extends BaseService {
makeAuth() { makeAuth() {
const auth = new Auth(Object.entries(this.newAuth)) const auth = new Auth(Object.entries(this.newAuth))
this.catchEmit(this.checkAuth(auth)).then(res => this.catchError(this.checkAuth(auth)).then(res =>
this.saveService({ name: this.mapServiceName(res, auth), auth })) this.saveService({ name: this.mapServiceName(res, auth), auth }))
} }

View File

@ -1,5 +1,5 @@
import { AxiosPromise, AxiosResponse } from 'axios' import { AxiosPromise, AxiosResponse } from 'axios'
import { unsafeAxiosMapper } from '../unsafeAxiosMapper' import { unsafeAxiosMapper } from '../unsafeMapper'
import ErrorLoadable from './ErrorLoadable' import ErrorLoadable from './ErrorLoadable'
export default class AxiosLoadable<T, E> extends ErrorLoadable<T, E> { export default class AxiosLoadable<T, E> extends ErrorLoadable<T, E> {

View File

@ -1,4 +1,5 @@
import Loadable from './Loadable' import Loadable from './Loadable'
import { unsafeMapper } from '../unsafeMapper';
export default class ErrorLoadable<T, E> extends Loadable<T> { export default class ErrorLoadable<T, E> extends Loadable<T> {

View File

@ -0,0 +1,19 @@
import { unsafeMapper } from '../unsafeMapper'
import ErrorLoadable from './ErrorLoadable'
export default class PromiseLoadable<T, E> extends ErrorLoadable<T, E> {
load<U>(promise: Promise<U>, then: (res: U) => T = res => unsafeMapper<T, U>(res), reset = true) {
if (reset) {
this.reset()
}
return promise
.then(res => this.success(then(res)))
.catch(err => {
this.fail(err)
throw err
})
}
}

View File

@ -1,7 +0,0 @@
import WebStorageHandler from './WebStorageHandler'
export default class LocalStorageHandler<T> extends WebStorageHandler<T> {
constructor(key: string, data: T, loader?: (value: any) => T, saver?: (me: T) => any) {
super(window.localStorage, key, data, loader, saver)
}
}

View File

@ -0,0 +1,31 @@
export default class StorageHandler<T> implements IStorageHandler<T> {
constructor(protected storage: Storage, protected key: string, protected fallback: T) { }
loadSync(): T {
const data = this.storage.getItem(this.key)
if (data) {
try {
return JSON.parse(data)
} catch (e) {
this.storage.removeItem(this.key)
}
}
return this.fallback;
}
async load() {
return this.loadSync()
}
save(data: T) {
return this.storage.setItem(this.key, JSON.stringify(data))
}
}
export interface IStorageHandler<T> {
load(): Promise<T>
loadSync(): T
save(data: T): void
}

View File

@ -1,33 +0,0 @@
export default class WebStorageHandler<T> { // TODO: extends loadable
constructor(protected storage: Storage, protected key: string, protected data: T,
protected loader: (value: any) => T = (v => v), protected saver: (me: T) => any = (v => v)) {
}
get() {
return this.data
}
set(value: T) {
this.data = value
this.save()
}
edit(mapper: (data: T) => T) {
this.set(mapper(this.get()))
}
load() {
const data = this.storage.getItem(this.key)
if (data) {
try {
this.data = this.loader(JSON.parse(data))
} catch (e) {
this.storage.removeItem(this.key)
}
}
}
save() {
this.storage.setItem(this.key, JSON.stringify(this.saver(this.data)))
}
}

View File

@ -1,5 +1,8 @@
import { AxiosResponse } from 'axios' import { AxiosResponse } from 'axios'
export function unsafeMapper<T, U = T>(res: U) {
return res as unknown as T
}
export function unsafeAxiosMapper<T, U = T>(res: AxiosResponse<U>) { export function unsafeAxiosMapper<T, U = T>(res: AxiosResponse<U>) {
return res.data as unknown as T return res.data as unknown as T
} }

View File

@ -83,7 +83,7 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
} }
get(path: string, options = {}) { get(path: string, options = {}) {
return this.catchEmit(this.rest.get(path, { params: { limit: this.options.buffer, ...options } })) return this.catchError(this.rest.get(path, { params: { limit: this.options.buffer, ...options } }))
} }
getMessages(channel: string, options = {}) { getMessages(channel: string, options = {}) {

View File

@ -57,11 +57,11 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
} }
get(path: string, options = {}) { get(path: string, options = {}) {
return this.catchEmit(this.rest.get(path, { params: { limit: this.options.buffer, ...options } })) return this.catchError(this.rest.get(path, { params: { limit: this.options.buffer, ...options } }))
} }
post(path: string, options = {}) { post(path: string, options = {}) {
return this.catchEmit(this.rest.post(path, options)) return this.catchError(this.rest.post(path, options))
} }
getTimeline(options = {}) { getTimeline(options = {}) {
@ -118,9 +118,9 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
break break
} }
} }
ws.onerror = ev => this.emitError(ev.type) ws.onerror = ev => this.addError(ev.type)
ws.onclose = () => { ws.onclose = () => {
this.emitError( this.addError(
'Mastodon stream disconnected !' + 'Mastodon stream disconnected !' +
(this.options.reconnect ? ' Reconnecting...' : '') (this.options.reconnect ? ' Reconnecting...' : '')
) )

View File

@ -94,7 +94,7 @@ export default class NextcloudNews extends Mixins<ConnectedService<object, objec
loadData() { loadData() {
this.news.load<{ items: News[] }>( this.news.load<{ items: News[] }>(
this.catchEmit(this.rest.get<News[]>('/items', { params: { batchSize: this.params.buffer, type: 3, getRead: false } })), this.catchError(this.rest.get<News[]>('/items', { params: { batchSize: this.params.buffer, type: 3, getRead: false } })),
res => res.data.items.map(n => { res => res.data.items.map(n => {
n.open = false n.open = false
return n return n
@ -107,7 +107,7 @@ export default class NextcloudNews extends Mixins<ConnectedService<object, objec
} }
makeRead(id: number) { makeRead(id: number) {
this.catchEmit(this.rest.put(`/items/${id}/read`)) this.catchError(this.rest.put(`/items/${id}/read`))
.then(() => this.removeNews(id)) .then(() => this.removeNews(id))
} }

View File

@ -25,7 +25,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import axios, { AxiosInstance, AxiosResponse } from 'axios' import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { Component } from 'vue-property-decorator' import { Component } from 'vue-property-decorator'
import ConnectedService from '@/components/service/ConnectedService' import ConnectedService from '@/components/service/ConnectedService'
@ -129,17 +129,21 @@ export default class OpenWeatherMap extends ConnectedService<object, object> {
this.loadForecast() this.loadForecast()
} }
get(path: string, params: AxiosRequestConfig) {
return this.catchError(this.rest.get(path, params))
}
getWeather(params: { id: number }) { getWeather(params: { id: number }) {
return this.catchEmit(this.rest.get('weather', { params })) return this.get('weather', { params })
} }
loadForecast() { loadForecast() {
const selected = this.weathers.map(w => w.selected, undefined) const selected = this.weathers.map(w => w.selected, undefined)
if(selected) { if(selected) {
this.forecast.load<{ list: Forecast[] }>( this.forecast.load<{ list: Forecast[] }>(
this.catchEmit(this.rest.get('forecast', { params: { this.get('forecast', { params: {
id: selected.id, cnt: this.params.forecastLimit id: selected.id, cnt: this.params.forecastLimit
}})), }}),
res => res.data.list res => res.data.list
) )
} else { } else {

25
src/store/App.ts Normal file
View File

@ -0,0 +1,25 @@
import { Selectable } from '@/helpers/lists/Selectable'
import ErrorLoadable from '@/helpers/loadable/ErrorLoadable'
import { Layout, Service } from '@/types/App'
import { Action, Module, Mutation, VuexModule } from 'vuex-module-decorators'
export interface AppState {
layouts: ErrorLoadable<Selectable<Layout>, string>
services: ErrorLoadable<Service[], string>
errors: string[]
}
@Module({ name: 'app', namespaced: true })
export default class App extends VuexModule implements AppState {
layouts = new ErrorLoadable<Selectable<Layout>, string>()
services = new ErrorLoadable<Service[], string>()
errors: string[] = []
addError(error: string) {
this.errors.push(error)
}
removeError(id: number) {
this.errors.splice(id, 1)
}
}

21
src/store/Errors.ts Normal file
View File

@ -0,0 +1,21 @@
import { Module, Mutation, VuexModule } from 'vuex-module-decorators'
export interface ErrorsState {
errors: string[]
}
@Module({ name: 'errors', namespaced: true })
export default class Errors extends VuexModule implements ErrorsState {
errors: string[] = []
@Mutation
add(error: string) {
this.errors.push(error)
}
@Mutation
remove(id: number) {
this.errors.splice(id, 1)
}
}

113
src/store/Layouts.ts Normal file
View File

@ -0,0 +1,113 @@
import { Selectable } from '@/helpers/lists/Selectable'
import PromiseLoadable from '@/helpers/loadable/PromiseLoadable'
import StorageHandler from '@/helpers/storage/StorageHandler'
import { Layout, Tile, Options, MoveType, MoveDirection } from '@/types/App'
import { Module, Mutation, VuexModule } from 'vuex-module-decorators'
export interface LayoutsState {
layouts: PromiseLoadable<Selectable<Layout>, string>
}
@Module({ name: 'services', namespaced: true })
export default class Layouts extends VuexModule implements LayoutsState {
layouts = new PromiseLoadable<Selectable<Layout>, string>() // TODO: create increment id storage
private storage = new StorageHandler<Layout[]>(window.localStorage, 'services', [{ name: 'main', tiles: [] }])
get data() {
return this.layouts.get()
}
get current() {
return this.layouts.map(l => l.selected, undefined)
}
get tiles() {
return this.current ? this.current.tiles : []
}
getTile(id: number) {
return this.tiles[id]
}
@Mutation
load() {
this.layouts.load(this.storage.load(),
data => new Selectable<Layout>(data))
}
save() {
this.storage.save(this.layouts.map(l => l.data, []))
}
@Mutation
select(id: number) {
this.layouts.with(l => l.select(id))
}
@Mutation
remove() {
this.layouts.with(l => l.remove())
this.save()
}
@Mutation
removeTile(id: number) {
const layout = this.current
if (layout) {
layout.tiles.splice(id, 1)
this.save()
}
}
@Mutation
add(layout: Layout) {
this.layouts.with(l => l.data.push(layout))
this.save()
}
@Mutation
addTile(tile: Tile) {
const layout = this.current
if (layout) {
layout.tiles.push(tile)
this.save()
}
}
@Mutation
setName(name: string) {
const layout = this.current
if (layout) {
layout.name = name
this.save()
}
}
@Mutation
setTileOption(id: number, key: string, value: any) {
const tile = this.getTile(id)
if (tile) {
tile.options[key] = value
this.save()
}
}
@Mutation
setTileOptions(id: number, options: Options) {
const tile = this.getTile(id)
if (tile) {
tile.options = options
this.save()
}
}
@Mutation
moveTile(id: number, type: MoveType, direction: MoveDirection) {
const tile = this.getTile(id)
if (tile) {
tile.position[type] = Math.max(1,
(tile.position[type] || 1) + direction
)
this.save()
}
}
}

67
src/store/Services.ts Normal file
View File

@ -0,0 +1,67 @@
import PromiseLoadable from '@/helpers/loadable/PromiseLoadable'
import StorageHandler from '@/helpers/storage/StorageHandler'
import { Auth, Service } from '@/types/App'
import { Module, Mutation, VuexModule } from 'vuex-module-decorators'
export interface ServicesState {
services: PromiseLoadable<Service[], string>
}
@Module({ name: 'services', namespaced: true })
export default class Services extends VuexModule implements ServicesState {
services = new PromiseLoadable<Service[], string>() // TODO: create increment id storage
private storage = new StorageHandler<Service[]>(window.localStorage, 'services', [])
get data() {
return this.services.get()
}
get(id: number) {
return this.services.map(data => data[id], undefined)
}
@Mutation
load() {
this.services.load(this.storage.load(),
data => data.map(s => ({ ...s, auth: new Auth(Object.entries(s.auth)) }))
)
}
save() {
function saveAuth(auth: Auth) {
const res: any = {}
for (const entry of auth.entries()) {
res[entry[0]] = entry[1]
}
return res
}
this.storage.save(this.services.map(data => data.map(
(s: Service) => ({ ...s, auth: saveAuth(s.auth) })
), []))
}
@Mutation
remove(id: number) {
this.services.with(data => data.splice(id, 1))
this.save()
}
@Mutation
add(service: Service): number {
this.services.with(data => data.push(service))
this.save()
return this.services.map(data => data.length - 1, -1)
}
@Mutation
set(id: number, name: string, auth: Auth) {
const service = this.get(id)
if (service) {
service.name = name
service.auth = auth
this.save()
}
}
}

View File

@ -9,14 +9,14 @@ export default class Time extends VuexModule implements TimeState {
now = new Date now = new Date
@Mutation @Mutation
updateTime() { update() {
this.now = new Date this.now = new Date
} }
@Action @Action
start() { start() {
setInterval(() => { setInterval(() => {
this.updateTime() this.update()
}, 15 * 1000) }, 15 * 1000)
} }

View File

@ -1,21 +1,39 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import { getModule } from 'vuex-module-decorators' import { getModule } from 'vuex-module-decorators'
import Errors from './Errors'
import Layouts from './Layouts'
import Services from './Services'
import Time from './Time' import Time from './Time'
Vue.use(Vuex) Vue.use(Vuex)
export interface State { export interface State {
errors: Errors
layouts: Layouts
services: Services
time: Time time: Time
} }
const store = new Vuex.Store<State>({ const store = new Vuex.Store<State>({
strict: process.env.NODE_ENV !== 'production', strict: process.env.NODE_ENV !== 'production',
modules: { modules: {
errors: Errors,
layouts: Layouts,
services: Services,
time: Time time: Time
} }
}) })
export const ErrorsModule = getModule(Errors, store)
export const LayoutsModule = getModule(Layouts, store)
LayoutsModule.load()
export const ServicesModule = getModule(Services, store)
ServicesModule.load()
export const TimeModule = getModule(Time, store) export const TimeModule = getModule(Time, store)
TimeModule.start() TimeModule.start()

View File

@ -15,13 +15,16 @@ export interface Layout {
tiles: Tile[] tiles: Tile[]
} }
export type MoveType = 'x' | 'y' | 'h' | 'w'
export type MoveDirection = 1 | -1
export interface Rect { export interface Rect {
x?: number, y?: number x?: number, y?: number
h?: number, w?: number h?: number, w?: number
} }
export interface Options { [index: string]: { option: any } }
export interface Tile { export interface Tile {
service: serviceKey service: serviceKey
position: Rect position: Rect
options: object options: Options
} }

View File

@ -1,49 +0,0 @@
import { ServiceData, tileKey } from './App';
export interface Message {
key: tileKey,
msg: any
}
export const SaveOptionEvent = 'saveOption'
export interface Option {
key: string
value: any
}
export interface SaveOptionMessage extends Message {
msg: Option
}
export const SaveOptionsEvent = 'saveOptions'
export interface SaveOptionsMessage extends Message {
msg: object
}
export const MoveTileEvent = 'move'
export type MoveType = 'x' | 'y' | 'h' | 'w'
export type MoveDirection = 1 | -1
export interface Move {
type: MoveType
direction: MoveDirection
}
export interface MoveTileMessage extends Message {
msg: Move
}
export const RemoveTileEvent = 'remove'
export interface RemoveTileMessage extends Message {
msg: undefined
}
export const SaveServiceEvent = 'saveService'
export interface SaveServiceMessage extends Message {
msg: ServiceData
}
export const RemoveServiceEvent = 'removeService'
export interface RemoveServiceMessage extends Message {
msg: undefined
}
export const ErrorEvent = 'error'
export interface ErrorMessage extends Message { }

View File

@ -1,4 +1,6 @@
module.exports = { module.exports = {
/* ... other settings */ transpileDependencies: [
transpileDependencies: ["vuex-module-decorators"] 'vuex-module-decorators',
'vuex-persist'
]
} }