Add layouts and more

This commit is contained in:
sheychen 2019-04-29 16:10:28 +02:00
parent 0c470f318a
commit 22a672788b
23 changed files with 551 additions and 430 deletions

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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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
} }
}, },

View File

@ -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
})
}
} }

View File

@ -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
})
}
}

View File

@ -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) {

View File

@ -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>

View File

@ -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) {

View File

@ -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))
} }
} }
} }

View File

@ -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)
} }
} }
} }

View File

@ -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()
})
} }
} }
} }

View File

@ -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()
}) })
} }
} }

View File

@ -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()
}) })
} }
} }

View File

@ -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>

View File

@ -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
View File

@ -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) {

View File

@ -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