Add layouts and more
This commit is contained in:
parent
0c470f318a
commit
22a672788b
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,35 @@
|
||||||
|
<script>
|
||||||
|
import Loadable from './loadable/Loadable.js'
|
||||||
|
import connectedServiceVue from './connectedService.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: connectedServiceVue,
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
account: new Loadable()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
connector() {
|
||||||
|
return this.account
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
load() {
|
||||||
|
this.account.load(this.catchEmit(this.getAccount(this.auth)), this.mapAccount)
|
||||||
|
},
|
||||||
|
checkAuth(auth) {
|
||||||
|
return this.getAccount(auth)
|
||||||
|
},
|
||||||
|
getAccount() {
|
||||||
|
this.mustDefine('getAccount(auth) method')
|
||||||
|
},
|
||||||
|
mapAccount(res) {
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
mapServiceName(res) {
|
||||||
|
return `${this.serviceName} ${this.mapAccount(res)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -22,7 +22,38 @@ export default {
|
||||||
settingInt: settingIntVue,
|
settingInt: settingIntVue,
|
||||||
settingString: settingStringVue
|
settingString: settingStringVue
|
||||||
},
|
},
|
||||||
extends: serviceEmiterVue
|
extends: serviceEmiterVue,
|
||||||
|
props: {
|
||||||
|
auth: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
newAuth: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
auth() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
this.mustDefine('init() method')
|
||||||
|
},
|
||||||
|
mustDefine(name) {
|
||||||
|
this.emitError('Must define ' + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Loadable = _Loadable
|
export const Loadable = _Loadable
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
<script>
|
||||||
|
import baseServiceVue from './baseService.vue'
|
||||||
|
import Loadable from './loadable/Loadable'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: baseServiceVue,
|
||||||
|
computed: {
|
||||||
|
isSetup() {
|
||||||
|
this.mustDefine('isSetup computed')
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
connector() {
|
||||||
|
this.mustDefine('connector computed')
|
||||||
|
return new Loadable()
|
||||||
|
},
|
||||||
|
serviceName() {
|
||||||
|
this.mustDefine('serviceName computed')
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
if(this.isSetup) {
|
||||||
|
this.load()
|
||||||
|
} else this.connector.fail('First connection')
|
||||||
|
},
|
||||||
|
makeAuth() {
|
||||||
|
this.catchEmit(this.checkAuth(this.newAuth)).then(res =>
|
||||||
|
this.saveService(this.mapServiceName(res, this.newAuth), this.newAuth))
|
||||||
|
},
|
||||||
|
load() {
|
||||||
|
this.mustDefine('load() method')
|
||||||
|
},
|
||||||
|
checkAuth() {
|
||||||
|
this.mustDefine('checkAuth(auth) method')
|
||||||
|
},
|
||||||
|
mapServiceName() {
|
||||||
|
return this.serviceName
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -16,7 +16,7 @@ export default {
|
||||||
mixins: [ timedMixin ],
|
mixins: [ timedMixin ],
|
||||||
props: {
|
props: {
|
||||||
date: {
|
date: {
|
||||||
type: Date,
|
type: [Date, Number, String],
|
||||||
default: Date.now
|
default: Date.now
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,7 +17,6 @@ export default class {
|
||||||
this.loaded = false
|
this.loaded = false
|
||||||
this.data = undefined
|
this.data = undefined
|
||||||
this.error = undefined
|
this.error = undefined
|
||||||
this.loadingMore = false
|
|
||||||
}
|
}
|
||||||
success(data) {
|
success(data) {
|
||||||
this.loaded = true
|
this.loaded = true
|
||||||
|
@ -39,12 +38,4 @@ export default class {
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMore(promise, then) {
|
|
||||||
this.loadingMore = true
|
|
||||||
promise.then(res => {
|
|
||||||
then(res, this.data, this)
|
|
||||||
this.loadingMore = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import Loadable from './Loadable'
|
||||||
|
|
||||||
|
export default class extends Loadable {
|
||||||
|
reset() {
|
||||||
|
super.reset()
|
||||||
|
this.loadingMore = false
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMore(promise, then) {
|
||||||
|
this.loadingMore = true
|
||||||
|
promise.then(res => {
|
||||||
|
then(res, this.data, this)
|
||||||
|
this.loadingMore = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,10 +11,7 @@ export default {
|
||||||
this.emit('error', err)
|
this.emit('error', err)
|
||||||
},
|
},
|
||||||
saveOptions(options) {
|
saveOptions(options) {
|
||||||
this.emit('saveAll', options)
|
this.emit('saveOptions', options)
|
||||||
this.$nextTick(function(){
|
|
||||||
this.$forceUpdate()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
saveOption(key, value) {
|
saveOption(key, value) {
|
||||||
this.saveOptionCouple({
|
this.saveOptionCouple({
|
||||||
|
@ -22,9 +19,11 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
saveOptionCouple(couple) {
|
saveOptionCouple(couple) {
|
||||||
this.emit('save', couple)
|
this.emit('saveOption', couple)
|
||||||
this.$nextTick(function(){
|
},
|
||||||
this.$forceUpdate()
|
saveService(name, auth) {
|
||||||
|
this.emit('saveService', {
|
||||||
|
name: name, auth: auth
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
catchEmit(req) {
|
catchEmit(req) {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
@keyup.left.ctrl.exact="onMove('y', -1)" @keyup.right.ctrl.exact="onMove('y', 1)"
|
@keyup.left.ctrl.exact="onMove('y', -1)" @keyup.right.ctrl.exact="onMove('y', 1)"
|
||||||
@keyup.up.alt.exact="onMove('h', -1)" @keyup.down.alt.exact="onMove('h', 1)"
|
@keyup.up.alt.exact="onMove('h', -1)" @keyup.down.alt.exact="onMove('h', 1)"
|
||||||
@keyup.left.alt.exact="onMove('w', -1)" @keyup.right.alt.exact="onMove('w', 1)"
|
@keyup.left.alt.exact="onMove('w', -1)" @keyup.right.alt.exact="onMove('w', 1)"
|
||||||
|
@keyup.delete.ctrl.exact="onRemove" @keyup.delete.alt.exact="onRemoveService"
|
||||||
)
|
)
|
||||||
slot(name="settings")
|
slot(name="settings")
|
||||||
</template>
|
</template>
|
||||||
|
@ -27,6 +28,12 @@ export default {
|
||||||
onMove(type, direction) {
|
onMove(type, direction) {
|
||||||
this.emit('move', { type: type, direction: direction })
|
this.emit('move', { type: type, direction: direction })
|
||||||
},
|
},
|
||||||
|
onRemove() {
|
||||||
|
this.emit('remove', { })
|
||||||
|
},
|
||||||
|
onRemoveService() {
|
||||||
|
this.emit('removeService', { })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
.client(@scroll.passive="onScroll")
|
.client(@scroll.passive="onScroll")
|
||||||
loadable-block.list(:loadable="guilds")
|
loadable-block.list(:loadable="guilds")
|
||||||
template(#success)
|
template(#success)
|
||||||
guild(v-for="guild in guilds.get()" :key="guild.id" :guild="guild" :showMedia="showMedia")
|
guild(v-for="guild in guilds.get()" :key="guild.id" :guild="guild" :showMedia="options.showMedia")
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -22,33 +22,18 @@ export default {
|
||||||
extends: serviceEmiterVue,
|
extends: serviceEmiterVue,
|
||||||
mixins: [ timerMinin ],
|
mixins: [ timerMinin ],
|
||||||
props: {
|
props: {
|
||||||
token: {
|
auth: {
|
||||||
default: undefined,
|
type: Object,
|
||||||
type: String
|
default: () => ({})
|
||||||
},
|
},
|
||||||
timeout: {
|
options: { type: Object, default: () => ({}) }
|
||||||
default: 5000,
|
|
||||||
type: Number
|
|
||||||
},
|
|
||||||
reconnect: {
|
|
||||||
default: false,
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
buffer: {
|
|
||||||
default: 20,
|
|
||||||
type: Number
|
|
||||||
},
|
|
||||||
showMedia: {
|
|
||||||
default: true,
|
|
||||||
type: Boolean
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
rest: axios.create({
|
rest: axios.create({
|
||||||
baseURL: 'https://discordapp.com/api/',
|
baseURL: 'https://discordapp.com/api/',
|
||||||
headers: { Authorization: this.token },
|
headers: { Authorization: this.auth.token },
|
||||||
timeout: this.timeout
|
timeout: this.options.timeout
|
||||||
}),
|
}),
|
||||||
guilds: new Loadable()
|
guilds: new Loadable()
|
||||||
}
|
}
|
||||||
|
@ -62,7 +47,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
get(path, options = {}) {
|
get(path, options = {}) {
|
||||||
return this.catchEmit(this.rest.get(path, { params: { limit: this.buffer, ...options } }))
|
return this.catchEmit(this.rest.get(path, { params: { limit: this.options.buffer, ...options } }))
|
||||||
},
|
},
|
||||||
onScroll() {
|
onScroll() {
|
||||||
/*if(!this.loadingOlder && event.target.scrollHeight - event.target.clientHeight - event.target.scrollTop - 100 < 0) {
|
/*if(!this.loadingOlder && event.target.scrollHeight - event.target.clientHeight - event.target.scrollTop - 100 < 0) {
|
||||||
|
|
|
@ -1,87 +1,53 @@
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
.discord
|
.discord
|
||||||
service-header(:emit="emit")
|
service-header(:emit="emit")
|
||||||
template(#title) Discord: {{ account.display() }}
|
template(#title) {{ serviceName }}: {{ account.display() }}
|
||||||
template(#settings)
|
template(#settings)
|
||||||
setting-boolean(:id="'reconnect'" :title="'Reconnect'" :value="reconnect" @change="saveOptionCouple")
|
setting-boolean(:id="'reconnect'" :title="'Reconnect'" :value="params.reconnect" @change="saveOptionCouple")
|
||||||
setting-int(:id="'buffer'" :title="'Buffer size'" :value="buffer" @change="saveOptionCouple")
|
setting-int(:id="'buffer'" :title="'Buffer size'" :value="params.buffer" @change="saveOptionCouple")
|
||||||
setting-boolean(:id="'showMedia'" :title="'Show medias'" :value="showMedia" @change="saveOptionCouple")
|
setting-boolean(:id="'showMedia'" :title="'Show medias'" :value="params.showMedia" @change="saveOptionCouple")
|
||||||
loadable-block.service-content(:loadable="account")
|
loadable-block.service-content(:loadable="account")
|
||||||
template(#success)
|
template(#success)
|
||||||
client(v-bind="$props")
|
client(:auth="auth" :options="params" :emit="emit")
|
||||||
template(#error)
|
template(#error)
|
||||||
form(@submit.prevent="makeAuth")
|
form(@submit.prevent="makeAuth")
|
||||||
p
|
p
|
||||||
label(for="token") Token:
|
label(for="token") Token:
|
||||||
input#token(v-model="newToken" required)
|
input#token(v-model="newAuth.token" required)
|
||||||
p
|
p
|
||||||
input(type="submit" value="Connect")
|
input(type="submit" value="Connect")
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* global axios */
|
/* global axios */
|
||||||
import baseServiceVue, { Loadable } from '../core/baseService.vue'
|
import accountServiceVue from '../core/accountService.vue'
|
||||||
|
|
||||||
import clientVue from './client.vue'
|
import clientVue from './client.vue'
|
||||||
|
|
||||||
export default { //TODO: Use oauth
|
export default { //TODO: Use oauth
|
||||||
name: 'Discord',
|
name: 'Discord',
|
||||||
components: {
|
components: { client: clientVue },
|
||||||
client: clientVue
|
extends: accountServiceVue,
|
||||||
},
|
computed: {
|
||||||
extends: baseServiceVue,
|
params() {
|
||||||
props: {
|
return { timeout: 5000, reconnect: false, buffer: 20, showMedia: true, ...this.options }
|
||||||
token: {
|
|
||||||
default: undefined,
|
|
||||||
type: String
|
|
||||||
},
|
},
|
||||||
timeout: {
|
isSetup() {
|
||||||
default: 5000,
|
return this.auth && this.auth.token
|
||||||
type: Number
|
|
||||||
},
|
},
|
||||||
reconnect: {
|
serviceName() {
|
||||||
default: false,
|
return 'Discord'
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
buffer: {
|
|
||||||
default: 20,
|
|
||||||
type: Number
|
|
||||||
},
|
|
||||||
showMedia: {
|
|
||||||
default: true,
|
|
||||||
type: Boolean
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
newToken: this.token,
|
|
||||||
account: new Loadable()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.init()
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
getMe(token) {
|
getAccount({ token }) {
|
||||||
return this.catchEmit(axios.get('https://discordapp.com/api/users/@me', {
|
return axios.get('https://discordapp.com/api/users/@me', {
|
||||||
headers: { Authorization: token },
|
headers: { Authorization: token },
|
||||||
timeout: this.timeout
|
timeout: this.params.timeout
|
||||||
}))
|
})
|
||||||
},
|
},
|
||||||
init() {
|
mapAccount(res) {
|
||||||
if(this.token) {
|
return res.data.username
|
||||||
this.account.load(
|
|
||||||
this.getMe(this.token),
|
|
||||||
res => res.data.username
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.account.fail('First connection')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
makeAuth() {
|
|
||||||
this.getMe(this.newToken)
|
|
||||||
.then(() => this.saveOption('token', this.newToken))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
loadable-block.list(:loadable="statues")
|
loadable-block.list(:loadable="statues")
|
||||||
template(#success)
|
template(#success)
|
||||||
template(v-for="status in statues.get()")
|
template(v-for="status in statues.get()")
|
||||||
status(v-if="showStatus(status)" :key="status.id" :status="status" :now="now" :showMedia="showMedia" @mark="onStatusMark")
|
status(v-if="showStatus(status)" :key="status.id" :status="status" :now="now" :showMedia="options.showMedia" @mark="onStatusMark")
|
||||||
.status(v-show="statues.loadingMore")
|
.status(v-show="statues.loadingMore")
|
||||||
.service-loader
|
.service-loader
|
||||||
.notifications(v-if="hasNotifications")
|
.notifications(v-if="hasNotifications")
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
span.date(@click.stop.prevent="onNotificationsClear") ❌
|
span.date(@click.stop.prevent="onNotificationsClear") ❌
|
||||||
.list
|
.list
|
||||||
notification(v-for="notification in notifications.get()" :key="notification.id" :notification="notification" :now="now"
|
notification(v-for="notification in notifications.get()" :key="notification.id" :notification="notification" :now="now"
|
||||||
:showMedia="showMedia" @dismiss="onNotificationDismiss" @mark="onStatusMark")
|
:showMedia="options.showMedia" @dismiss="onNotificationDismiss" @mark="onStatusMark")
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -28,6 +28,7 @@ import statusVue from './status.vue'
|
||||||
import notificationVue from './notification.vue'
|
import notificationVue from './notification.vue'
|
||||||
|
|
||||||
import Loadable from '../core/loadable/Loadable'
|
import Loadable from '../core/loadable/Loadable'
|
||||||
|
import ReLoadable from '../core/loadable/ReLoadable'
|
||||||
import loadableBlockVue from '../core/loadable/loadableBlock.vue'
|
import loadableBlockVue from '../core/loadable/loadableBlock.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -39,35 +40,20 @@ export default {
|
||||||
extends: serviceEmiterVue,
|
extends: serviceEmiterVue,
|
||||||
mixins: [ timerMinin ],
|
mixins: [ timerMinin ],
|
||||||
props: {
|
props: {
|
||||||
server: {
|
auth: {
|
||||||
type: String,
|
type: Object,
|
||||||
default: undefined
|
default: () => ({})
|
||||||
},
|
},
|
||||||
token: {
|
options: { type: Object, default: () => ({}) }
|
||||||
type: String,
|
|
||||||
default: undefined
|
|
||||||
},
|
|
||||||
timeout: {
|
|
||||||
type: Number,
|
|
||||||
default: 5000
|
|
||||||
},
|
|
||||||
reconnect: Boolean,
|
|
||||||
buffer: {
|
|
||||||
type: Number,
|
|
||||||
default: 20
|
|
||||||
},
|
|
||||||
reblog: Boolean,
|
|
||||||
reply: Boolean,
|
|
||||||
showMedia: Boolean
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
rest: axios.create({
|
rest: axios.create({
|
||||||
baseURL: `https://${this.server}/api/v1/`,
|
baseURL: `https://${this.auth.server}/api/v1/`,
|
||||||
headers: { Authorization: 'Bearer ' + this.token },
|
headers: { Authorization: 'Bearer ' + this.auth.token },
|
||||||
timeout: this.timeout
|
timeout: this.options.timeout
|
||||||
}),
|
}),
|
||||||
statues: new Loadable(),
|
statues: new ReLoadable(),
|
||||||
notifications: new Loadable()
|
notifications: new Loadable()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -89,7 +75,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
get(path, options = {}) {
|
get(path, options = {}) {
|
||||||
return this.catchEmit(this.rest.get(path, { params: { limit: this.buffer, ...options } }))
|
return this.catchEmit(this.rest.get(path, { params: { limit: this.options.buffer, ...options } }))
|
||||||
},
|
},
|
||||||
post(path, options = {}) {
|
post(path, options = {}) {
|
||||||
return this.catchEmit(this.rest.post(path, options))
|
return this.catchEmit(this.rest.post(path, options))
|
||||||
|
@ -104,14 +90,14 @@ export default {
|
||||||
(res, statues) => Lists.pushAll(statues, res.data)
|
(res, statues) => Lists.pushAll(statues, res.data)
|
||||||
)
|
)
|
||||||
} else if(event.target.scrollTop < 20) {
|
} else if(event.target.scrollTop < 20) {
|
||||||
this.statues.get().splice(this.buffer)
|
this.statues.get().splice(this.options.buffer)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeById(ls, id) {
|
removeById(ls, id) {
|
||||||
Lists.removeFirst(ls, e => e.id === id)
|
Lists.removeFirst(ls, e => e.id === id)
|
||||||
},
|
},
|
||||||
showStatus(status) {
|
showStatus(status) {
|
||||||
return (!status.in_reply_to_id || this.reply) && (!status.reblog || this.reblog)
|
return (!status.in_reply_to_id || this.options.reply) && (!status.reblog || this.options.reblog)
|
||||||
},
|
},
|
||||||
onStatusMark(action) {
|
onStatusMark(action) {
|
||||||
this.post(`/statuses/${action.id}/${action.type}`)
|
this.post(`/statuses/${action.id}/${action.type}`)
|
||||||
|
@ -127,7 +113,7 @@ export default {
|
||||||
},
|
},
|
||||||
setupStream() {
|
setupStream() {
|
||||||
const ws = new WebSocket(
|
const ws = new WebSocket(
|
||||||
`wss://${this.server}/api/v1/streaming?access_token=${this.token}&stream=user`
|
`wss://${this.auth.server}/api/v1/streaming?access_token=${this.auth.token}&stream=user`
|
||||||
)
|
)
|
||||||
ws.onmessage = event => {
|
ws.onmessage = event => {
|
||||||
event = JSON.parse(event.data)
|
event = JSON.parse(event.data)
|
||||||
|
@ -150,9 +136,9 @@ export default {
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
this.emitError(
|
this.emitError(
|
||||||
'Mastodon stream disconnected !' +
|
'Mastodon stream disconnected !' +
|
||||||
(this.reconnect ? ' Reconnecting...' : '')
|
(this.options.reconnect ? ' Reconnecting...' : '')
|
||||||
)
|
)
|
||||||
if (this.reconnect) setTimeout(() => this.setupStream(), this.timeout)
|
if (this.options.reconnect) setTimeout(() => this.setupStream(), this.options.timeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,113 +2,64 @@
|
||||||
.mastodon
|
.mastodon
|
||||||
service-header(:emit="emit")
|
service-header(:emit="emit")
|
||||||
template(#title)
|
template(#title)
|
||||||
| Mastodon:
|
| {{ serviceName }}:
|
||||||
loadable-inline(:loadable="account")
|
loadable-inline(:loadable="account")
|
||||||
template(#success)
|
template(#success)
|
||||||
span(v-html="parseEmojis(account.data.display_name, account.data.emojis) + '@' + server")
|
span(v-html="parseEmojis(account.data.display_name, account.data.emojis) + '@' + auth.server")
|
||||||
template(#settings)
|
template(#settings)
|
||||||
setting-boolean(:id="'reconnect'" :title="'Reconnect'" :value="reconnect" @change="saveOptionCouple")
|
setting-boolean(:id="'reconnect'" :title="'Reconnect'" :value="params.reconnect" @change="saveOptionCouple")
|
||||||
setting-boolean(:id="'reblog'" :title="'Show reblogs'" :value="reblog" @change="saveOptionCouple")
|
setting-boolean(:id="'reblog'" :title="'Show reblogs'" :value="params.reblog" @change="saveOptionCouple")
|
||||||
setting-boolean(:id="'reply'" :title="'Show replies'" :value="reply" @change="saveOptionCouple")
|
setting-boolean(:id="'reply'" :title="'Show replies'" :value="params.reply" @change="saveOptionCouple")
|
||||||
setting-int(:id="'buffer'" :title="'Buffer size'" :value="buffer" @change="saveOptionCouple")
|
setting-int(:id="'buffer'" :title="'Buffer size'" :value="params.buffer" @change="saveOptionCouple")
|
||||||
setting-boolean(:id="'showMedia'" :title="'Show medias'" :value="showMedia" @change="saveOptionCouple")
|
setting-boolean(:id="'showMedia'" :title="'Show medias'" :value="params.showMedia" @change="saveOptionCouple")
|
||||||
loadable-block.service-content(:loadable="account")
|
loadable-block.service-content(:loadable="account")
|
||||||
template(#success)
|
template(#success)
|
||||||
client(v-bind="$props")
|
client(:auth="auth" :options="params" :emit="emit")
|
||||||
template(#error)
|
template(#error)
|
||||||
form(@submit.prevent="makeAuth")
|
form(@submit.prevent="makeAuth")
|
||||||
p
|
p
|
||||||
label(for="server") Server:
|
label(for="server") Server:
|
||||||
input#server(v-model="newServer" required)
|
input#server(v-model="newAuth.server" required)
|
||||||
p
|
p
|
||||||
label(for="token") Token:
|
label(for="token") Token:
|
||||||
input#token(v-model="newToken" required)
|
input#token(v-model="newAuth.token" required)
|
||||||
p
|
p
|
||||||
input(type="submit" value="Connect")
|
input(type="submit" value="Connect")
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* global axios */
|
/* global axios */
|
||||||
import baseServiceVue, { Loadable } from '../core/baseService.vue'
|
import accountServiceVue from '../core/accountService.vue'
|
||||||
|
|
||||||
import { parseEmojisMixin } from './tools'
|
import { parseEmojisMixin } from './tools'
|
||||||
import clientVue from './client.vue'
|
import clientVue from './client.vue'
|
||||||
|
|
||||||
export default { //TODO: Use oauth
|
export default { //TODO: Use oauth
|
||||||
name: 'Mastodon',
|
name: 'Mastodon',
|
||||||
components: {
|
components: { client: clientVue },
|
||||||
client: clientVue
|
extends: accountServiceVue,
|
||||||
},
|
|
||||||
extends: baseServiceVue,
|
|
||||||
mixins: [ parseEmojisMixin ],
|
mixins: [ parseEmojisMixin ],
|
||||||
props: {
|
computed: {
|
||||||
server: {
|
params() {
|
||||||
type: String,
|
return { timeout: 5000, reconnect: false, buffer: 20, reblog: true, reply: false,
|
||||||
default: undefined
|
showMedia: true, ...this.options }
|
||||||
},
|
},
|
||||||
token: {
|
isSetup() {
|
||||||
type: String,
|
return this.auth && this.auth.server && this.auth.token
|
||||||
default: undefined
|
|
||||||
},
|
},
|
||||||
timeout: {
|
serviceName() {
|
||||||
default: 5000,
|
return 'Mastodon'
|
||||||
type: Number
|
|
||||||
},
|
|
||||||
reconnect: {
|
|
||||||
default: false,
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
buffer: {
|
|
||||||
default: 20,
|
|
||||||
type: Number
|
|
||||||
},
|
|
||||||
reblog: {
|
|
||||||
default: true,
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
reply: {
|
|
||||||
default: false,
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
showMedia: {
|
|
||||||
default: true,
|
|
||||||
type: Boolean
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
newServer: this.server,
|
|
||||||
newToken: this.token,
|
|
||||||
account: new Loadable()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.init()
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
getMe(server, token) {
|
getAccount({ server, token }) {
|
||||||
return this.catchEmit(axios.get(`https://${server}/api/v1/accounts/verify_credentials`, {
|
return axios.get(`https://${server}/api/v1/accounts/verify_credentials`, {
|
||||||
headers: { Authorization: 'Bearer ' + token },
|
headers: { Authorization: 'Bearer ' + token },
|
||||||
timeout: this.timeout
|
timeout: this.params.timeout
|
||||||
}))
|
})
|
||||||
},
|
},
|
||||||
init() {
|
mapServiceName(res, { server }) {
|
||||||
if(this.server && this.token) {
|
return `${this.serviceName} ${this.mapAccount(res).acct}@${server}`
|
||||||
this.account.load(
|
|
||||||
this.getMe(this.server, this.token),
|
|
||||||
res => res.data
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.account.fail('First connection')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
makeAuth() {
|
|
||||||
this.getMe(this.newServer, this.newToken)
|
|
||||||
.then(() => {
|
|
||||||
this.saveOptions({ ...this.$props,
|
|
||||||
server: this.newServer, token: this.newToken })
|
|
||||||
this.init()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
.nextcloud-news(v-show="showEmpty || hasNews || !isSetup")
|
.nextcloud-news(v-show="showService")
|
||||||
service-header(:emit="emit")
|
service-header(:emit="emit")
|
||||||
template(#title)
|
template(#title)
|
||||||
| Nextcloud News
|
| {{ serviceName }}
|
||||||
span.note(v-if="hasNews") ({{ news.get().length }})
|
span.note(v-if="hasNews") ({{ news.get().length }})
|
||||||
template(#settings)
|
template(#settings)
|
||||||
setting-int(:id="'update'" :title="'Update interval'" :value="update" @change="saveOptionCouple")
|
setting-int(:id="'update'" :title="'Update interval'" :value="params.update" @change="saveOptionCouple")
|
||||||
setting-int(:id="'buffer'" :title="'Buffer size'" :value="buffer" @change="saveOptionCouple")
|
setting-int(:id="'buffer'" :title="'Buffer size'" :value="params.buffer" @change="saveOptionCouple")
|
||||||
setting-boolean(:id="'showEmpty'" :title="'Show empty'" :value="showEmpty" @change="saveOptionCouple")
|
setting-boolean(:id="'showEmpty'" :title="'Show empty'" :value="params.showEmpty" @change="saveOptionCouple")
|
||||||
loadable-block.unreaded(:loadable="news")
|
loadable-block.unreaded(:loadable="news")
|
||||||
template(#success)
|
template(#success)
|
||||||
.news(v-for="line in news.get()")
|
.news(v-for="line in news.get()")
|
||||||
|
@ -20,20 +20,20 @@
|
||||||
form(@submit.prevent="makeAuth")
|
form(@submit.prevent="makeAuth")
|
||||||
p
|
p
|
||||||
label(for="server") Server:
|
label(for="server") Server:
|
||||||
input#server(v-model="newServer" required)
|
input#server(v-model="newAuth.server" required)
|
||||||
p
|
p
|
||||||
label(for="username") Username:
|
label(for="username") Username:
|
||||||
input#username(v-model="newUsername" required)
|
input#username(v-model="newAuth.username" required)
|
||||||
p
|
p
|
||||||
label(for="token") Token:
|
label(for="token") Token:
|
||||||
input#token(v-model="newToken" required)
|
input#token(v-model="newAuth.token" required)
|
||||||
p
|
p
|
||||||
input(type="submit" value="Connect")
|
input(type="submit" value="Connect")
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* global axios */
|
/* global axios */
|
||||||
import baseServiceVue from '../core/baseService.vue'
|
import connectedServiceVue from '../core/connectedService.vue'
|
||||||
import fromNowVue, { timerMinin } from '../core/fromNow.vue'
|
import fromNowVue, { timerMinin } from '../core/fromNow.vue'
|
||||||
|
|
||||||
import Loadable from '../core/loadable/Loadable'
|
import Loadable from '../core/loadable/Loadable'
|
||||||
|
@ -44,68 +44,38 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
fromNow: fromNowVue
|
fromNow: fromNowVue
|
||||||
},
|
},
|
||||||
extends: baseServiceVue,
|
extends: connectedServiceVue,
|
||||||
mixins: [ timerMinin ],
|
mixins: [ timerMinin ],
|
||||||
props: {
|
|
||||||
server: {
|
|
||||||
type: String,
|
|
||||||
default: undefined
|
|
||||||
},
|
|
||||||
username: {
|
|
||||||
type: String,
|
|
||||||
default: undefined
|
|
||||||
},
|
|
||||||
token: {
|
|
||||||
type: String,
|
|
||||||
default: undefined
|
|
||||||
},
|
|
||||||
timeout: {
|
|
||||||
default: 5000,
|
|
||||||
type: Number
|
|
||||||
},
|
|
||||||
buffer : {
|
|
||||||
default: -1,
|
|
||||||
type: Number
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
default: 5 * 60, //5min
|
|
||||||
type: Number
|
|
||||||
},
|
|
||||||
showEmpty: {
|
|
||||||
default: false,
|
|
||||||
type: Boolean
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
rest: axios.create({
|
rest: undefined, //NOTE: set in this.init()
|
||||||
baseURL: `https://${this.server}/index.php/apps/news/api/v1-2/`,
|
news: new Loadable()
|
||||||
timeout: this.timeout,
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Basic ' + btoa(this.username + ':' + this.token)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
news: new Loadable(),
|
|
||||||
newServer: this.server,
|
|
||||||
newUsername: this.username,
|
|
||||||
newToken: this.token,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
params() {
|
||||||
|
return { timeout: 5000, buffer: -1, update: 5 * 60, showEmpty: true, ...this.options }
|
||||||
|
},
|
||||||
|
isSetup() {
|
||||||
|
return this.auth && this.auth.server && this.auth.username && this.auth.token
|
||||||
|
},
|
||||||
|
connector() {
|
||||||
|
return this.news
|
||||||
|
},
|
||||||
|
serviceName() {
|
||||||
|
return 'Nextcloud News'
|
||||||
|
},
|
||||||
hasNews() {
|
hasNews() {
|
||||||
return this.news.isSuccess() && this.news.get().length > 0
|
return this.news.isSuccess() && this.news.get().length > 0
|
||||||
},
|
},
|
||||||
isSetup() {
|
showService() {
|
||||||
return this.server && this.username && this.token
|
return this.params.showEmpty || this.hasNews || !this.isSetup
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
this.init()
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
loadData() {
|
loadData() {
|
||||||
this.news.load(
|
this.news.load(
|
||||||
this.catchEmit(this.rest.get('/items', { params: { batchSize: this.buffer, type: 3, getRead: false } })),
|
this.catchEmit(this.rest.get('/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
|
||||||
|
@ -119,22 +89,24 @@ export default {
|
||||||
this.catchEmit(this.rest.put(`/items/${id}/read`))
|
this.catchEmit(this.rest.put(`/items/${id}/read`))
|
||||||
.then(() => this.removeNews(id))
|
.then(() => this.removeNews(id))
|
||||||
},
|
},
|
||||||
init() {
|
load() {
|
||||||
if(this.isSetup) {
|
this.rest = axios.create({
|
||||||
this.loadData()
|
baseURL: `https://${this.auth.server}/index.php/apps/news/api/v1-2/`,
|
||||||
|
timeout: this.params.timeout,
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Basic ' + btoa(this.auth.username + ':' + this.auth.token)
|
||||||
|
}
|
||||||
|
}) //NOTE: required by this.params
|
||||||
|
|
||||||
if(this.update > 0)
|
this.loadData()
|
||||||
setInterval(this.loadData, this.update * 1000)
|
|
||||||
}else this.news.fail('First connection')
|
if(this.params.update > 0)
|
||||||
|
setInterval(this.loadData, this.params.update * 1000)
|
||||||
},
|
},
|
||||||
makeAuth() {
|
checkAuth({ server, username, token }){
|
||||||
this.catchEmit(axios.get(`https://${this.newServer}/index.php/apps/news/api/v1-2/folders`, {
|
return axios.get(`https://${server}/index.php/apps/news/api/v1-2/folders`, {
|
||||||
headers: { Authorization: 'Basic ' + btoa(this.newUsername + ':' + this.newToken) },
|
headers: { Authorization: 'Basic ' + btoa(username + ':' + token) },
|
||||||
timeout: this.timeout
|
timeout: this.params.timeout
|
||||||
})).then(() => {
|
|
||||||
this.saveOptions({ ...this.$props,
|
|
||||||
server: this.newServer, token: this.newToken, username: this.newUsername })
|
|
||||||
this.init()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
.openweathermap
|
.openweathermap
|
||||||
service-header(:emit="emit")
|
service-header(:emit="emit")
|
||||||
template(#title) OpenWeatherMap
|
template(#title) {{ serviceName }}
|
||||||
template(#settings)
|
template(#settings)
|
||||||
setting-int(:id="'update'" :title="'Update interval'" :value="update" @change="saveOptionCouple")
|
setting-int(:id="'update'" :title="'Update interval'" :value="params.update" @change="saveOptionCouple")
|
||||||
setting-int(:id="'forecastLimit'" :title="'Forecast limit'" :value="forecastLimit" @change="saveOptionCouple")
|
setting-int(:id="'forecastLimit'" :title="'Forecast limit'" :value="params.forecastLimit" @change="saveOptionCouple")
|
||||||
p.setting
|
p.setting
|
||||||
button(@click="showAdd = true") Add city
|
button(@click="showAdd = true") Add city
|
||||||
loadable-block(:loadable="weathers")
|
loadable-block(:loadable="weathers")
|
||||||
|
@ -20,14 +20,14 @@
|
||||||
form(@submit.prevent="makeAuth")
|
form(@submit.prevent="makeAuth")
|
||||||
p
|
p
|
||||||
label(for="token") Token:
|
label(for="token") Token:
|
||||||
input#token(v-model="newToken" required)
|
input#token(v-model="newAuth.token" required)
|
||||||
p
|
p
|
||||||
input(type="submit" value="Connect")
|
input(type="submit" value="Connect")
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* global axios */
|
/* global axios */
|
||||||
import baseServiceVue from '../core/baseService.vue'
|
import connectedServiceVue from '../core/connectedService.vue'
|
||||||
import Lists from '../core/Lists.js'
|
import Lists from '../core/Lists.js'
|
||||||
import Loadable from '../core/loadable/Loadable'
|
import Loadable from '../core/loadable/Loadable'
|
||||||
|
|
||||||
|
@ -40,52 +40,30 @@ export default {
|
||||||
weather: weatherVue,
|
weather: weatherVue,
|
||||||
chart: chartVue
|
chart: chartVue
|
||||||
},
|
},
|
||||||
extends: baseServiceVue,
|
extends: connectedServiceVue,
|
||||||
props: {
|
|
||||||
token: {
|
|
||||||
type: String,
|
|
||||||
default: undefined
|
|
||||||
},
|
|
||||||
cities: {
|
|
||||||
type: Array,
|
|
||||||
default: function () {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
timeout: {
|
|
||||||
default: 5000,
|
|
||||||
type: Number
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
default: 10 * 60, //10min
|
|
||||||
type: Number
|
|
||||||
},
|
|
||||||
lang: {
|
|
||||||
default: 'fr',
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
forecastLimit: {
|
|
||||||
default: 9,
|
|
||||||
type: Number
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
rest: axios.create({
|
rest: undefined, //NOTE: set in this.init()
|
||||||
baseURL: 'https://api.openweathermap.org/data/2.5/',
|
|
||||||
params: {
|
|
||||||
appid: this.token, units: 'metric', lang: this.lang
|
|
||||||
},
|
|
||||||
timeout: this.timeout
|
|
||||||
}),
|
|
||||||
newToken: this.token,
|
|
||||||
weathers: new Loadable(),
|
weathers: new Loadable(),
|
||||||
forecast: new Loadable(),
|
forecast: new Loadable(),
|
||||||
selectedId: 0,
|
selectedId: 0,
|
||||||
showAdd: this.cities.length == 0
|
showAdd: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
params() {
|
||||||
|
return { cities: [], timeout: 5000, update: 10 * 60, lang: 'fr',
|
||||||
|
forecastLimit: 9, ...this.options }
|
||||||
|
},
|
||||||
|
isSetup() {
|
||||||
|
return this.auth && this.auth.token
|
||||||
|
},
|
||||||
|
serviceName() {
|
||||||
|
return 'OpenWeatherMap'
|
||||||
|
},
|
||||||
|
connector() {
|
||||||
|
return this.weathers
|
||||||
|
},
|
||||||
forecastChart() { return {
|
forecastChart() { return {
|
||||||
datasets: [{
|
datasets: [{
|
||||||
type: 'line',
|
type: 'line',
|
||||||
|
@ -117,7 +95,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.init()
|
this.$watch('options.cities', this.init)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
makeSelect(id) {
|
makeSelect(id) {
|
||||||
|
@ -138,7 +116,7 @@ export default {
|
||||||
if(this.selected) {
|
if(this.selected) {
|
||||||
this.forecast.load(
|
this.forecast.load(
|
||||||
this.catchEmit(this.rest.get('forecast', { params: {
|
this.catchEmit(this.rest.get('forecast', { params: {
|
||||||
id: this.selected.id, cnt: this.forecastLimit
|
id: this.selected.id, cnt: this.params.forecastLimit
|
||||||
}})),
|
}})),
|
||||||
res => res.data.list
|
res => res.data.list
|
||||||
)
|
)
|
||||||
|
@ -149,34 +127,38 @@ export default {
|
||||||
return `${date.toLocaleDateString()} ${date.getHours()}h`
|
return `${date.toLocaleDateString()} ${date.getHours()}h`
|
||||||
},
|
},
|
||||||
addCity(id) {
|
addCity(id) {
|
||||||
this.cities.push({ id: id })
|
this.params.cities.push({ id: id })
|
||||||
this.saveOption('cities', this.cities)
|
this.saveOption('cities', this.params.cities)
|
||||||
},
|
},
|
||||||
removeCity(key) {
|
removeCity(key) {
|
||||||
Lists.removeAt(this.cities, key)
|
Lists.removeAt(this.params.cities, key)
|
||||||
this.saveOption('cities', this.cities)
|
this.saveOption('cities', this.params.cities)
|
||||||
},
|
},
|
||||||
init() {
|
load() {
|
||||||
if(this.token) {
|
this.rest = axios.create({
|
||||||
if(this.cities.length > 0) {
|
baseURL: 'https://api.openweathermap.org/data/2.5/',
|
||||||
axios.all(this.cities.map(city => this.getWeather(city)))
|
params: {
|
||||||
.then(axios.spread((...ress) =>
|
appid: this.auth.token, units: 'metric', lang: this.params.lang
|
||||||
this.weathers.success(ress.map(r => r.data))))
|
},
|
||||||
.then(this.loadForecast)
|
timeout: this.params.timeout
|
||||||
.catch(this.weathers.fail)
|
}) //NOTE: required by this.params
|
||||||
|
this.showAdd = this.params.cities.length == 0
|
||||||
|
|
||||||
if(this.update > 0)
|
if(this.params.cities.length > 0) {
|
||||||
setInterval(this.updateData, this.update * 1000)
|
axios.all(this.params.cities.map(city => this.getWeather(city)))
|
||||||
} else this.weathers.success([])
|
.then(axios.spread((...ress) =>
|
||||||
} else this.weathers.fail('First connection')
|
this.weathers.success(ress.map(r => r.data))))
|
||||||
|
.then(this.loadForecast)
|
||||||
|
.catch(this.weathers.fail)
|
||||||
|
|
||||||
|
if(this.update > 0)
|
||||||
|
setInterval(this.updateData, this.params.update * 1000)
|
||||||
|
} else this.weathers.success([])
|
||||||
},
|
},
|
||||||
makeAuth() {
|
checkAuth({ token }) {
|
||||||
this.catchEmit(axios.get('https://api.openweathermap.org/data/2.5/weather', {
|
return axios.get('https://api.openweathermap.org/data/2.5/weather', {
|
||||||
params: { q: 'London', appid: this.newToken },
|
params: { q: 'London', appid: token },
|
||||||
timeout: this.timeout
|
timeout: this.params.timeout
|
||||||
})).then(() => {
|
|
||||||
this.saveOption('token', this.newToken)
|
|
||||||
this.init()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
38
index.html
38
index.html
|
@ -21,15 +21,35 @@
|
||||||
<div id="errors" v-show="errors">
|
<div id="errors" v-show="errors">
|
||||||
<p class="error" v-for="(error, key) in errors" @click="removeError(key)">{{ error }}</p>
|
<p class="error" v-for="(error, key) in errors" @click="removeError(key)">{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="manager">
|
<div id="content">
|
||||||
<button @click="showManager = !showManager">+</button>
|
<div id="services">
|
||||||
<input v-show="showManager" v-model="newService" @keyup.enter="addService">
|
<div class="tile" v-if="layout" v-for="tile in layoutTiles" :style="tile.grid">
|
||||||
</div>
|
<component :is="tile.service.type" :emit="tile.emiter" :auth="tile.service.auth" :options="tile.options" />
|
||||||
<div id="services">
|
</div>
|
||||||
<component
|
</div>
|
||||||
v-for="(service, key) in services" :is="service.type" :emit="makeEmiter(key)"
|
<button id="showManager" @click="showManager = !showManager">{{ showManager ? '▼' : '▲' }}</button>
|
||||||
:key="key" v-bind="service.options" :style="gridPos(key, service.position)"
|
<div id="manager" v-show="showManager">
|
||||||
/>
|
<div>
|
||||||
|
<input v-model="newService" @keyup.enter="addService" placeholder="service">
|
||||||
|
</div>
|
||||||
|
<div id="layout-select">
|
||||||
|
<div class="layout" v-for="(layout, id) in layouts">
|
||||||
|
<template v-if="layoutId == id">
|
||||||
|
<input :value="layout.name" @keyup.ctrl.delete="removeSelectedLayout()"
|
||||||
|
@keyup.enter="renameSelectedLayout($event.target.value)">
|
||||||
|
</template>
|
||||||
|
<button v-else @click="layoutId = id">{{ layout.name }}</button>
|
||||||
|
</div>
|
||||||
|
<div><button @click="addLayout">+</button></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<select @change="showService($event.target.value)">
|
||||||
|
<option v-for="(service, key) in services.get()" :value="key">
|
||||||
|
{{ service.name || service.type }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="main.js"></script>
|
<script src="main.js"></script>
|
||||||
|
|
51
main.css
51
main.css
|
@ -56,13 +56,46 @@ input, select, button {
|
||||||
padding: 0.3em;
|
padding: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#manager {
|
#content {
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-box-direction: normal;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#showManager {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#manager {
|
||||||
|
background-color: #222;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
padding-left: 1em;
|
||||||
|
height: 1.3em;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-pack: justify;
|
||||||
|
-ms-flex-pack: justify;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#layout-select {
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
#services {
|
#services {
|
||||||
height: 100vh;
|
-webkit-box-flex: 1;
|
||||||
|
-ms-flex: 1;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
display: -ms-grid;
|
display: -ms-grid;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: .2em;
|
grid-gap: .2em;
|
||||||
|
@ -73,8 +106,12 @@ input, select, button {
|
||||||
justify-items: stretch;
|
justify-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
#services > div {
|
#services .tile {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#services .tile > div {
|
||||||
|
height: 100%;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -ms-flexbox;
|
display: -ms-flexbox;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -84,25 +121,25 @@ input, select, button {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
#services > div .service-header .title, #services > div .service-header .settings {
|
#services .tile > div .service-header .title, #services .tile > div .service-header .settings {
|
||||||
margin: 0.3em;
|
margin: 0.3em;
|
||||||
background-color: #222;
|
background-color: #222;
|
||||||
border-radius: 0.3em;
|
border-radius: 0.3em;
|
||||||
padding: 0.3em;
|
padding: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#services > div .service-header .title {
|
#services .tile > div .service-header .title {
|
||||||
font-size: large;
|
font-size: large;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
#services > div .service-header .settings .position {
|
#services .tile > div .service-header .settings .position {
|
||||||
float: right;
|
float: right;
|
||||||
width: 1.2em;
|
width: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#services > div .service-content {
|
#services .tile > div .service-content {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
178
main.js
178
main.js
|
@ -1,31 +1,148 @@
|
||||||
/* globals Vue */
|
/* globals Vue */
|
||||||
/* exported app */
|
/* exported app */
|
||||||
|
|
||||||
|
const layoutsStorage = 'layouts'
|
||||||
const servicesStorage = 'services'
|
const servicesStorage = 'services'
|
||||||
|
|
||||||
|
class WebStorageHandler { //TODO: extends loadable
|
||||||
|
constructor(storage, key, data) {
|
||||||
|
this.storage = storage
|
||||||
|
this.key = key
|
||||||
|
this.data = data || []
|
||||||
|
}
|
||||||
|
|
||||||
|
get() {
|
||||||
|
return this.data
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
if (this.storage.getItem(this.key)) {
|
||||||
|
try {
|
||||||
|
this.data = JSON.parse(this.storage.getItem(this.key))
|
||||||
|
} catch (e) {
|
||||||
|
this.storage.removeItem(this.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.storage.setItem(this.key, JSON.stringify(this.data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var app = new Vue({
|
var app = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
showManager: false,
|
showManager: false,
|
||||||
|
|
||||||
|
layouts: new WebStorageHandler(window.localStorage, layoutsStorage, [{ name: 'main', tiles: [] }]),
|
||||||
|
layoutId: 0,
|
||||||
|
|
||||||
|
services: new WebStorageHandler(window.localStorage, servicesStorage),
|
||||||
newService: '',
|
newService: '',
|
||||||
services: [],
|
|
||||||
errors: [],
|
errors: [],
|
||||||
bus: new Vue()
|
bus: new Vue()
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (localStorage.getItem(servicesStorage)) { //TODO: allow external storage
|
this.layouts.load()
|
||||||
try {
|
this.services.load()
|
||||||
this.services = JSON.parse(localStorage.getItem(servicesStorage))
|
|
||||||
} catch (e) {
|
|
||||||
localStorage.removeItem(servicesStorage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bus.$on('error', this.onError)
|
this.bus.$on('error', this.onError)
|
||||||
this.bus.$on('saveAll', this.onSaveAll)
|
this.bus.$on('saveOptions', this.onSaveOptions)
|
||||||
this.bus.$on('save', this.onSave)
|
this.bus.$on('saveOption', this.onSaveOption)
|
||||||
this.bus.$on('move', this.onMove)
|
this.bus.$on('move', this.onMove)
|
||||||
|
this.bus.$on('remove', this.onRemove)
|
||||||
|
this.bus.$on('saveService', this.onSaveService)
|
||||||
|
this.bus.$on('removeService', this.onRemoveService)
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
layout() {
|
||||||
|
return this.layouts.get()[this.layoutId]
|
||||||
|
},
|
||||||
|
layoutTiles() {
|
||||||
|
return this.layout.tiles.map((tile, key) => ({
|
||||||
|
...tile, service: this.loadService(key, tile.service),
|
||||||
|
grid: this.gridPos(tile.position), emiter: this.makeEmiter(key)
|
||||||
|
}))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
//Layouts
|
||||||
|
addLayout() {
|
||||||
|
this.layouts.get().push({
|
||||||
|
name: 'layout' + this.layouts.get().length, tiles: []
|
||||||
|
})
|
||||||
|
this.layouts.save()
|
||||||
|
},
|
||||||
|
renameSelectedLayout(name) {
|
||||||
|
this.layout.name = name
|
||||||
|
this.layouts.save()
|
||||||
|
},
|
||||||
|
removeSelectedLayout() {
|
||||||
|
this.layouts.get().splice(this.layoutId, 1)
|
||||||
|
this.layoutId = 0
|
||||||
|
this.layouts.save()
|
||||||
|
},
|
||||||
|
//Tiles
|
||||||
|
showService(id) {
|
||||||
|
this.layout.tiles.push({
|
||||||
|
service: id, position: {}, options: {}
|
||||||
|
})
|
||||||
|
this.layouts.save()
|
||||||
|
},
|
||||||
|
tile(id) {
|
||||||
|
return this.layout.tiles[id]
|
||||||
|
},
|
||||||
|
onSaveOption({ key, msg }) {
|
||||||
|
this.$set(this.tile(key).options, msg.key, msg.value)
|
||||||
|
this.layouts.save()
|
||||||
|
},
|
||||||
|
onSaveOptions({ key, msg }) {
|
||||||
|
this.tile(key).options = Object.assign({}, this.tile(key).options, msg)
|
||||||
|
this.layouts.save()
|
||||||
|
},
|
||||||
|
onMove({ key, msg }) {
|
||||||
|
this.$set(this.tile(key).position, msg.type, Math.max(1,
|
||||||
|
(this.tile(key).position[msg.type] || 1) + msg.direction
|
||||||
|
))
|
||||||
|
this.layouts.save()
|
||||||
|
},
|
||||||
|
onRemove({ key }) {
|
||||||
|
this.layout.tiles.splice(key, 1)
|
||||||
|
this.layouts.save()
|
||||||
|
},
|
||||||
|
|
||||||
|
//Services
|
||||||
|
loadService(key, id) {
|
||||||
|
const ser = this.services.get()[id]
|
||||||
|
if (ser)
|
||||||
|
return ser
|
||||||
|
else {
|
||||||
|
this.onRemove({ key })
|
||||||
|
this.addError('Removing missing service')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addService() {
|
||||||
|
if (this.newService) {
|
||||||
|
this.services.get().push({ type: this.newService, name: this.newService, auth: {} })
|
||||||
|
this.services.save()
|
||||||
|
this.showService(this.services.get().length - 1)
|
||||||
|
this.newService = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSaveService({ key, msg }) {
|
||||||
|
const service = this.loadService(key, this.tile(key).service)
|
||||||
|
service.name = msg.name
|
||||||
|
service.auth = msg.auth
|
||||||
|
this.services.save()
|
||||||
|
},
|
||||||
|
onRemoveService({ key }) {
|
||||||
|
this.services.get().splice(this.tile(key).service, 1)
|
||||||
|
this.onRemove({ key })
|
||||||
|
this.services.save()
|
||||||
|
},
|
||||||
|
|
||||||
//Errors
|
//Errors
|
||||||
onError(event) {
|
onError(event) {
|
||||||
this.addError(event.msg.toString())
|
this.addError(event.msg.toString())
|
||||||
|
@ -36,47 +153,12 @@ var app = new Vue({
|
||||||
removeError(id) {
|
removeError(id) {
|
||||||
this.errors.splice(id, 1)
|
this.errors.splice(id, 1)
|
||||||
},
|
},
|
||||||
//Services
|
|
||||||
addService() {
|
|
||||||
if (!this.newService)
|
|
||||||
return
|
|
||||||
|
|
||||||
this.services.push({
|
//Helpers
|
||||||
type: this.newService,
|
gridPos(position = {}) {
|
||||||
options: {}, position: {}
|
|
||||||
})
|
|
||||||
this.newService = ''
|
|
||||||
this.showManager = false
|
|
||||||
this.saveServices()
|
|
||||||
},
|
|
||||||
onSave({ key, msg }) {
|
|
||||||
this.$set(this.services[key].options, msg.key, msg.value)
|
|
||||||
this.saveServices()
|
|
||||||
},
|
|
||||||
onSaveAll({ key, msg }) {
|
|
||||||
this.$set(this.services, key, {
|
|
||||||
...this.services[key],
|
|
||||||
options: msg
|
|
||||||
})
|
|
||||||
this.saveServices()
|
|
||||||
},
|
|
||||||
onMove({ key, msg }) {
|
|
||||||
this.$set(this.services[key].position, msg.type, Math.max(1,
|
|
||||||
(this.services[key].position[msg.type] || 1) + msg.direction
|
|
||||||
))
|
|
||||||
this.saveServices()
|
|
||||||
},
|
|
||||||
removeService(id) {
|
|
||||||
this.services.splice(id, 1)
|
|
||||||
this.saveServices()
|
|
||||||
},
|
|
||||||
saveServices() {
|
|
||||||
localStorage.setItem(servicesStorage, JSON.stringify(this.services))
|
|
||||||
},
|
|
||||||
gridPos(id, position = {}) {
|
|
||||||
return {
|
return {
|
||||||
'grid-row': `${position.x || 1} / span ${position.h || 2}`,
|
'grid-row': `${position.x || 1} / span ${position.h || 2}`,
|
||||||
'grid-column': `${position.y || id*2+1} / span ${position.w || 2}`
|
'grid-column': `${position.y || 1} / span ${position.w || 2}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
makeEmiter(key) {
|
makeEmiter(key) {
|
||||||
|
|
53
main.sass
53
main.sass
|
@ -57,33 +57,52 @@ input, select, button
|
||||||
.error
|
.error
|
||||||
@include tile
|
@include tile
|
||||||
|
|
||||||
#manager
|
#content
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
height: 100vh
|
||||||
|
|
||||||
|
#showManager
|
||||||
position: absolute
|
position: absolute
|
||||||
bottom: 0
|
bottom: 0
|
||||||
|
|
||||||
|
#manager
|
||||||
|
background-color: $tileColor
|
||||||
|
border-radius: $borderRadius
|
||||||
|
padding-left: 1em
|
||||||
|
height: 1.3em
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
|
||||||
|
#layout-select
|
||||||
|
display: flex
|
||||||
|
|
||||||
#services
|
#services
|
||||||
height: 100vh
|
flex: 1
|
||||||
|
overflow: hidden
|
||||||
display: grid
|
display: grid
|
||||||
grid-gap: .2em
|
grid-gap: .2em
|
||||||
grid-template-columns: repeat(8, minmax(0, 1fr))
|
grid-template-columns: repeat(8, minmax(0, 1fr))
|
||||||
grid-template-rows: repeat(4, minmax(0, 1fr))
|
grid-template-rows: repeat(4, minmax(0, 1fr))
|
||||||
justify-items: stretch
|
justify-items: stretch
|
||||||
& > div
|
.tile
|
||||||
overflow: auto
|
overflow: auto
|
||||||
display: flex
|
& > div
|
||||||
flex-direction: column
|
height: 100%
|
||||||
.service-header
|
display: flex
|
||||||
.title, .settings
|
flex-direction: column
|
||||||
@include tile
|
.service-header
|
||||||
.title
|
.title, .settings
|
||||||
font-size: large
|
@include tile
|
||||||
text-align: center
|
.title
|
||||||
font-weight: bold
|
font-size: large
|
||||||
.settings .position
|
text-align: center
|
||||||
float: right
|
font-weight: bold
|
||||||
width: 1.2em
|
.settings .position
|
||||||
.service-content
|
float: right
|
||||||
overflow: hidden
|
width: 1.2em
|
||||||
|
.service-content
|
||||||
|
overflow: hidden
|
||||||
.service-loader
|
.service-loader
|
||||||
display: inline-block
|
display: inline-block
|
||||||
width: 64px
|
width: 64px
|
||||||
|
|
Loading…
Reference in New Issue