Loadable, more auth and starting discord

This commit is contained in:
sheychen 2019-04-17 18:01:14 +02:00
parent edc9a87db8
commit 67e6379e41
21 changed files with 617 additions and 209 deletions

1
compiler/dist/discord/main.js vendored Normal file

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,27 @@
export default {
last(list) {
return list[list.length - 1]
},
getIndex(list, where) {
for(let i = list.length - 1; i >= 0; i--)
if(where(list[i])) return i
return list.length
},
removeAt(list, id) {
list.splice(id, 1)
},
removeFirst(list, where) {
this.removeAt(list, this.getIndex(list, where))
},
pushAll(list, elems) {
list.push.apply(list, elems)
},
clear(list) {
list.splice(0, list.length)
},
for(list, action) {
for(let i = 0; i < list.length; i++)
action(list[i], i, list)
}
}

View File

@ -1,18 +1,29 @@
<script> <script>
import serviceEmiterVue from './serviceEmiter.vue' import serviceEmiterVue from './serviceEmiter.vue'
import serviceHeaderVue from '../core/serviceHeader.vue' import _Loadable from './loadable/Loadable.js'
import settingBooleanVue from '../core/input/settingBoolean.vue' import loadableBlockVue from './loadable/loadableBlock.vue'
import settingIntVue from '../core/input/settingInt.vue' import loadableInlineVue from './loadable/loadableInline.vue'
import serviceHeaderVue from './serviceHeader.vue'
import settingBooleanVue from './input/settingBoolean.vue'
import settingIntVue from './input/settingInt.vue'
import settingStringVue from './input/settingString.vue' import settingStringVue from './input/settingString.vue'
export default { export default {
extends: serviceEmiterVue, extends: serviceEmiterVue,
components: { components: {
loadableBlock: loadableBlockVue,
loadableInline: loadableInlineVue,
serviceHeader: serviceHeaderVue, serviceHeader: serviceHeaderVue,
settingBoolean: settingBooleanVue, settingBoolean: settingBooleanVue,
settingInt: settingIntVue, settingInt: settingIntVue,
settingString: settingStringVue settingString: settingStringVue
} }
} }
export const Loadable = _Loadable
</script> </script>

View File

@ -0,0 +1,50 @@
export default class {
constructor() {
this.reset()
}
get() {
return this.data
}
isSuccess() {
return this.loaded && this.error == undefined
}
display() {
return this.loaded ? (this.error || this.data) : 'Loading...'
}
reset() {
this.loaded = false
this.data = undefined
this.error = undefined
this.loadingMore = false
}
success(data) {
this.loaded = true
this.data = data || {}
}
fail(error) {
this.loaded = true
this.error = error || 'Failed'
}
load(promise, then, reset = true) {
if(reset)
this.reset()
promise
.then(res => this.success(then(res)))
.catch(err => {
this.fail(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,22 @@
<template lang="pug">
div.loadable-block
slot(name="success" v-if="loadable.isSuccess()") {{ get }}
slot(name="error" v-else-if="loadable.error") {{ loadable.error }}
slot(name="loading" v-else)
.service-loader
</template>
<script>
import Loadable from './Loadable.js'
export default {
props: {
loadable: Loadable
},
computed: {
get() {
return this.loadable.get()
}
}
}
</script>

View File

@ -0,0 +1,21 @@
<template lang="pug">
span.loadable-inline
slot(name="success" v-if="loadable.isSuccess()") {{ get }}
slot(name="error" v-else-if="loadable.error") {{ loadable.error }}
slot(name="loading" v-else) Loading...
</template>
<script>
import Loadable from './Loadable.js'
export default {
props: {
loadable: Loadable
},
computed: {
get() {
return this.loadable.get()
}
}
}
</script>

View File

@ -0,0 +1,98 @@
<template lang="pug">
.client(@scroll.passive="onScroll")
.list(v-if="messages.length > 0")
template(v-for="message in messages")
message(:key="message.id" :message="message" :now="now" :showMedia="showMedia")
.message(v-show="loadingOlder")
.service-loader
.service-loader(v-else)
</template>
<script>
import { timerMinin } from '../core/fromNow.vue'
import serviceEmiterVue from '../core/serviceEmiter.vue'
import messageVue from './message.vue'
export default {
extends: serviceEmiterVue,
mixins: [ timerMinin ],
components: {
message: messageVue
},
props: {
token: String,
timeout: Number,
reconnect: Boolean,
buffer: Number,
showMedia: Boolean
},
data() {
return {
rest: axios.create({
baseURL: 'https://discordapp.com/api/',
timeout: this.timeout,
headers: {
Authorization: this.token
}
}),
now: Date.now(),
messages: [],
loadingOlder: false
};
},
methods: {
onScroll(event) {
/*if(!this.loadingOlder && event.target.scrollHeight - event.target.clientHeight - event.target.scrollTop - 100 < 0) {
this.loadingOlder = true
this.catchEmit(this.rest.get("/timelines/home", { params: { limit: this.buffer,
max_id: this.statues[this.statues.length - 1].id
} })).then(res => {
this.statues.push.apply(this.statues, res.data)
this.loadingOlder = false
})
} else if(event.target.scrollTop < 20) {
this.statues.splice(this.buffer)
}*/
},
setupStream() {
/*const ws = new WebSocket(
`wss://${this.server}/api/v1/streaming?access_token=${this.token}&stream=user`
)
ws.onmessage = event => {
event = JSON.parse(event.data)
const payload = JSON.parse(event.payload)
switch (event.event) {
case "update":
this.statues.unshift(payload)
break
case "notification":
this.notifications.unshift(payload)
break
case "delete":
this.removeStatus(payload)
break
}
};
ws.onerror = this.emitError
ws.onclose = () => {
this.emitError(
"Mastodon stream disconnected !" +
(this.reconnect ? " Reconnecting..." : "")
)
if (this.reconnect) setTimeout(() => this.setupStream(), this.timeout)
}*/
}
},
created() {
/*this.rest
.get("/timelines/home", { params: { limit: this.buffer } })
.then(res => this.statues.push.apply(this.statues, res.data))
.catch(this.emitError)*/
this.setupStream()
}
}
</script>

View File

@ -0,0 +1,84 @@
<template lang="pug">
.discord
service-header(:emit="emit")
template(#title) Discord: {{ account.display() }}
template(#settings)
setting-boolean(:id="'reconnect'" :title="'Reconnect'" :value="reconnect" @change="saveOptionCouple")
setting-int(:id="'buffer'" :title="'Buffer size'" :value="buffer" @change="saveOptionCouple")
setting-boolean(:id="'showMedia'" :title="'Show medias'" :value="showMedia" @change="saveOptionCouple")
loadable-block.service-content(:loadable="account")
template(#success)
client(v-bind="$props")
template(#error)
form(@submit.prevent="makeAuth")
p
label(for="token") Token:
input#token(v-model="newToken" required)
p
input(type="submit" value="Connect")
</template>
<script>
import baseServiceVue, { Loadable } from '../core/baseService.vue'
import clientVue from './client.vue'
export default { //TODO: Use oauth
name: 'discord',
extends: baseServiceVue,
components: {
client: clientVue
},
props: {
token: String,
timeout: {
default: 5000,
type: Number
},
reconnect: {
default: false,
type: Boolean
},
buffer: {
default: 20,
type: Number
},
showMedia: {
default: true,
type: Boolean
}
},
data() {
return {
valid: false,
newToken: this.token,
account: new Loadable()
}
},
methods: {
getMe(token) {
return this.catchEmit(axios.get('https://discordapp.com/api/users/@me', {
headers: { Authorization: token },
timeout: this.timeout
}))
},
init() {
if(this.token) {
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))
}
},
created() {
this.init()
}
}
</script>

View File

@ -0,0 +1,22 @@
<template lang="pug">
.message {{ message }}
</template>
<script>
import fromNowVue, { timedMixin } from '../core/fromNow.vue'
export default {
mixins: [ timedMixin ],
components: {
fromNow: fromNowVue
},
props: {
message: Object,
showMedia: {
type: Boolean,
default: true
}
},
methods: { }
}
</script>

View File

@ -1,19 +1,19 @@
<template lang="pug"> <template lang="pug">
.client(@scroll.passive="onScroll") .client(@scroll.passive="onScroll")
.statues .statues
.header(v-if="notifications.length > 0") Accueil .header(v-if="hasNotifications") Accueil
.list(v-if="statues.length > 0") loadable-block.list(:loadable="statues")
template(v-for="status in statues") template(#success)
status(v-if="showStatus(status)" :key="status.id" :status="status" :now="now" :showMedia="showMedia" @mark="onStatusMark") template(v-for="status in statues.get()")
.status(v-show="loadingOlder") status(v-if="showStatus(status)" :key="status.id" :status="status" :now="now" :showMedia="showMedia" @mark="onStatusMark")
.service-loader .status(v-show="statues.loadingMore")
.service-loader(v-else) .service-loader
.notifications(v-if="notifications.length > 0") .notifications(v-if="hasNotifications")
.header .header
| Notifications | Notifications
span.date(@click.stop.prevent="onNotificationsClear") span.date(@click.stop.prevent="onNotificationsClear")
.list .list
notification(v-for="notification in notifications" :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="showMedia" @dismiss="onNotificationDismiss" @mark="onStatusMark")
</template> </template>
@ -21,15 +21,21 @@
import { timerMinin } from '../core/fromNow.vue' import { timerMinin } from '../core/fromNow.vue'
import serviceEmiterVue from '../core/serviceEmiter.vue' import serviceEmiterVue from '../core/serviceEmiter.vue'
import Lists from '../core/Lists'
import statusVue from './status.vue' import statusVue from './status.vue'
import notificationVue from './notification.vue' import notificationVue from './notification.vue'
import Loadable from '../core/loadable/Loadable'
import loadableBlockVue from '../core/loadable/loadableBlock.vue'
export default { export default {
extends: serviceEmiterVue, extends: serviceEmiterVue,
mixins: [ timerMinin ], mixins: [ timerMinin ],
components: { components: {
status: statusVue, status: statusVue,
notification: notificationVue notification: notificationVue,
loadableBlock: loadableBlockVue
}, },
props: { props: {
server: String, server: String,
@ -45,62 +51,55 @@ export default {
return { return {
rest: axios.create({ rest: axios.create({
baseURL: `https://${this.server}/api/v1/`, baseURL: `https://${this.server}/api/v1/`,
timeout: this.timeout, headers: { Authorization: 'Bearer ' + this.token },
headers: { timeout: this.timeout
Authorization: "Bearer " + this.token
}
}), }),
statues: [], statues: new Loadable(),
notifications: [], notifications: new Loadable()
now: Date.now(),
loadingOlder: false
}; };
}, },
computed: {
hasNotifications() {
return this.notifications.isSuccess() && this.notifications.get().length > 0
}
},
methods: { methods: {
get(path, options = {}) {
return this.catchEmit(this.rest.get(path, { params: { limit: this.buffer, ...options } }))
},
post(path, options = {}) {
return this.catchEmit(this.rest.post(path, options))
},
getTimeline(options) {
return this.get('/timelines/home', options)
},
onScroll(event) { onScroll(event) {
if(!this.loadingOlder && event.target.scrollHeight - event.target.clientHeight - event.target.scrollTop - 100 < 0) { if(!this.statues.loadingMore && event.target.scrollHeight - event.target.clientHeight - event.target.scrollTop - 100 < 0) {
this.loadingOlder = true this.statues.loadMore(
this.catchEmit(this.rest this.getTimeline({ max_id: Lists.last(this.statues.get()).id }),
.get("/timelines/home", { params: { limit: this.buffer, (res, statues) => Lists.pushAll(statues, res.data)
max_id: this.statues[this.statues.length - 1].id } })) )
.then(res => {
this.statues.push.apply(this.statues, res.data)
this.loadingOlder = false
})
} else if(event.target.scrollTop < 20) { } else if(event.target.scrollTop < 20) {
this.statues.splice(this.buffer) this.statues.get().splice(this.buffer)
} }
}, },
removeStatus(id) { removeById(ls, id) {
for (var i = this.statues.length - 1; i >= 0; i--) { Lists.removeFirst(ls, e => e.id === id)
if (this.statues[i].id === id) {
this.statues.splice(i, 1)
break
}
}
},
removeNotification(id) {
for (var i = this.notifications.length - 1; i >= 0; i--) {
if (this.notifications[i].id === id) {
this.notifications.splice(i, 1)
break
}
}
}, },
showStatus(status) { showStatus(status) {
return (!status.in_reply_to_id || this.reply) && (!status.reblog || this.reblog) return (!status.in_reply_to_id || this.reply) && (!status.reblog || this.reblog)
}, },
onStatusMark(action) { onStatusMark(action) {
this.catchEmit(this.rest.post(`/statuses/${action.id}/${action.type}`)) this.post(`/statuses/${action.id}/${action.type}`)
.then(action.callback) .then(action.callback)
}, },
onNotificationDismiss(id) { onNotificationDismiss(id) {
this.catchEmit(this.rest.post('/notifications/dismiss', { id: id })) this.post('/notifications/dismiss', { id: id })
.then(() => this.removeNotification(id)) .then(() => this.removeById(this.notifications.get(), id))
}, },
onNotificationsClear() { onNotificationsClear() {
this.catchEmit(this.rest.post('/notifications/clear')) this.post('/notifications/clear')
.then(() => this.notifications.splice(0, this.notifications.length)) .then(() => Lists.clear(this.notifications.get()))
}, },
setupStream() { setupStream() {
const ws = new WebSocket( const ws = new WebSocket(
@ -110,35 +109,37 @@ export default {
event = JSON.parse(event.data) event = JSON.parse(event.data)
const payload = JSON.parse(event.payload) const payload = JSON.parse(event.payload)
switch (event.event) { switch (event.event) {
case "update": case 'update':
this.statues.unshift(payload) this.statues.get().unshift(payload)
break break
case "notification": case 'notification':
this.notifications.unshift(payload) this.notifications.get().unshift(payload)
break break
case "delete": case 'delete':
this.removeStatus(payload) this.removeById(this.statues.get(), id)
break break
} }
}; };
ws.onerror = this.emitError ws.onerror = this.emitError
ws.onclose = () => { ws.onclose = () => {
this.emitError( this.emitError(
"Mastodon stream disconnected !" + 'Mastodon stream disconnected !' +
(this.reconnect ? " Reconnecting..." : "") (this.reconnect ? ' Reconnecting...' : '')
) )
if (this.reconnect) setTimeout(() => this.setupStream(), this.timeout) if (this.reconnect) setTimeout(() => this.setupStream(), this.timeout)
} }
} }
}, },
created() { created() {
this.catchEmit(this.rest.get("/timelines/home", { params: { limit: this.buffer } })) this.statues.load(
.then(res => this.statues.push.apply(this.statues, res.data)) this.getTimeline({}),
res => res.data)
this.catchEmit(this.rest.get("/notifications", { params: { limit: this.buffer } })) this.notifications.load(
.then(res => this.notifications.push.apply(this.notifications, res.data)) this.get('/notifications'),
res => res.data)
this.setupStream() this.setupStream()
} }

View File

@ -3,29 +3,32 @@
service-header(:emit="emit") service-header(:emit="emit")
template(#title) template(#title)
| Mastodon: | Mastodon:
span(v-html="parseEmojis(account.display_name, account.emojis)") loadable-inline(:loadable="account")
| {{ server ? '@' + server : '' }} template(#success)
span(v-html="parseEmojis(account.data.display_name, account.data.emojis) + '@' + server")
template(#settings) template(#settings)
setting-boolean(:id="'reconnect'" :title="'Reconnect'" :value="reconnect" @change="saveOptionCouple") setting-boolean(:id="'reconnect'" :title="'Reconnect'" :value="reconnect" @change="saveOptionCouple")
setting-boolean(:id="'reblog'" :title="'Show reblogs'" :value="reblog" @change="saveOptionCouple") setting-boolean(:id="'reblog'" :title="'Show reblogs'" :value="reblog" @change="saveOptionCouple")
setting-boolean(:id="'reply'" :title="'Show replies'" :value="reply" @change="saveOptionCouple") setting-boolean(:id="'reply'" :title="'Show replies'" :value="reply" @change="saveOptionCouple")
setting-int(:id="'buffer'" :title="'Buffer size'" :value="buffer" @change="saveOptionCouple") setting-int(:id="'buffer'" :title="'Buffer size'" :value="buffer" @change="saveOptionCouple")
setting-boolean(:id="'showMedia'" :title="'Show medias'" :value="showMedia" @change="saveOptionCouple") setting-boolean(:id="'showMedia'" :title="'Show medias'" :value="showMedia" @change="saveOptionCouple")
client(v-if="server && token" v-bind="$props") loadable-block.service-content(:loadable="account")
.auth(v-else) template(#success)
form(@submit.prevent="makeAuth") client(v-bind="$props")
p template(#error)
label(for="server") Server: form(@submit.prevent="makeAuth")
input#server(v-model="newServer" required) p
p label(for="server") Server:
label(for="token") Token: input#server(v-model="newServer" required)
input#token(v-model="newToken" required) p
p label(for="token") Token:
input(type="submit" value="Connect") input#token(v-model="newToken" required)
p
input(type="submit" value="Connect")
</template> </template>
<script> <script>
import baseServiceVue from '../core/baseService.vue' import baseServiceVue, { Loadable } from '../core/baseService.vue'
import { parseEmojisMixin } from './tools' import { parseEmojisMixin } from './tools'
import clientVue from './client.vue' import clientVue from './client.vue'
@ -69,7 +72,7 @@ export default { //TODO: Use oauth
return { return {
newServer: this.server, newServer: this.server,
newToken: this.token, newToken: this.token,
account: { display_name: 'Loading...', emojis: [] } account: new Loadable()
}; };
}, },
methods: { methods: {
@ -79,20 +82,27 @@ export default { //TODO: Use oauth
timeout: this.timeout timeout: this.timeout
})) }))
}, },
init() {
if(this.server && this.token) {
this.account.load(
this.getMe(this.server, this.token),
res => res.data
)
} else {
this.account.fail('First connection')
}
},
makeAuth() { makeAuth() {
this.getMe(this.newServer, this.newToken) this.getMe(this.newServer, this.newToken)
.then(() => this.saveOptions({ ...this.$props, .then(() => {
server: this.newServer, token: this.newToken })) this.saveOptions({ ...this.$props,
server: this.newServer, token: this.newToken })
this.init()
})
} }
}, },
created() { created() {
if(this.server && this.token) { this.init()
this.getMe(this.server, this.token)
.then(res => this.account = res.data)
.catch(() => this.account.display_name = 'Failed')
} else{
this.account.display_name = 'First connection'
}
} }
} }
</script> </script>

View File

@ -1,39 +1,43 @@
<template lang="pug"> <template lang="pug">
.nextcloud-news(v-show="showEmpty || unreaded.length > 0 || !server || !token || !username") .nextcloud-news(v-show="showEmpty || hasNews || !isSetup")
service-header(:emit="emit") service-header(:emit="emit")
template(#title) template(#title)
| Nextcloud News | Nextcloud News
span.note(v-if="unreaded.length > 0") ({{ unreaded.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="update" @change="saveOptionCouple")
setting-int(:id="'buffer'" :title="'Buffer size'" :value="buffer" @change="saveOptionCouple") setting-int(:id="'buffer'" :title="'Buffer size'" :value="buffer" @change="saveOptionCouple")
setting-boolean(:id="'showEmpty'" :title="'Show empty'" :value="showEmpty" @change="saveOptionCouple") setting-boolean(:id="'showEmpty'" :title="'Show empty'" :value="showEmpty" @change="saveOptionCouple")
.unreaded loadable-block.unreaded(:loadable="news")
.news(v-for="news in unreaded") template(#success)
a(:href="news.url" target="_blank") .news(v-for="line in news.get()")
from-now.date(:date="news.pubDate * 1000" :now="now") a(:href="line.url" target="_blank")
span.read(@click.stop="makeRead(news.id)") &#128065; from-now.date(:date="line.pubDate * 1000" :now="now")
span.title(@click.stop="news.open = !news.open") {{ news.author }} {{ news.title }} span.read(@click.stop="makeRead(line.id)") &#128065;
.content(v-if="news.open && news.body") {{ news.body }} span.title(@click.stop="line.open = !line.open") {{ line.author }} {{ line.title }}
.auth(v-if="!server") .content(v-if="line.open && line.body") {{ line.body }}
form(@submit.prevent="setServer") template(#error)
p form(@submit.prevent="makeAuth")
label(for="server") Server: p
input#server(v-model="newServer" required) label(for="server") Server:
p input#server(v-model="newServer" required)
label(for="username") Username: p
input#username(v-model="newUsername" required) label(for="username") Username:
p input#username(v-model="newUsername" required)
label(for="token") Token: p
input#token(v-model="newToken" required) label(for="token") Token:
p input#token(v-model="newToken" required)
input(type="submit" value="Connect") p
input(type="submit" value="Connect")
</template> </template>
<script> <script>
import baseServiceVue from '../core/baseService.vue' import baseServiceVue from '../core/baseService.vue'
import fromNowVue, { timerMinin } from '../core/fromNow.vue' import fromNowVue, { timerMinin } from '../core/fromNow.vue'
import Loadable from '../core/loadable/Loadable'
import Lists from '../core/Lists'
export default { export default {
name: 'nextcloud-news', name: 'nextcloud-news',
extends: baseServiceVue, extends: baseServiceVue,
@ -71,48 +75,58 @@ export default {
Authorization: 'Basic ' + btoa(this.username + ':' + this.token) Authorization: 'Basic ' + btoa(this.username + ':' + this.token)
} }
}), }),
unreaded: [], news: new Loadable(),
now: Date.now(),
newServer: this.server, newServer: this.server,
newUsername: this.username, newUsername: this.username,
newToken: this.token, newToken: this.token,
}; };
}, },
computed: {
hasNews() {
return this.news.isSuccess() && this.news.get().length > 0
},
isSetup() {
return this.server && this.username && this.token
}
},
methods: { methods: {
loadData() { loadData() {
this.catchEmit(this.rest.get("/items", { params: { batchSize: this.buffer, type: 3, getRead: false } })) this.news.load(
.then(res => this.unreaded = res.data.items.map(n => { this.catchEmit(this.rest.get("/items", { params: { batchSize: this.buffer, type: 3, getRead: false } })),
res => res.data.items.map(n => {
n.open = false n.open = false
return n return n
})) })
)
}, },
removeNews(id) { removeNews(id) {
for (var i = this.unreaded.length - 1; i >= 0; i--) { Lists.removeFirst(this.news.get(), n => n.id === id)
if (this.unreaded[i].id === id) {
this.unreaded.splice(i, 1)
break
}
}
}, },
makeRead(id) { makeRead(id) {
this.catchEmit(this.rest.put(`/items/${id}/read`)) this.catchEmit(this.rest.put(`/items/${id}/read`))
.then(() => this.removeNews(id)) .then(() => this.removeNews(id))
}, },
setServer() { init() {
if(this.isSetup) {
this.loadData()
if(this.update > 0)
setInterval(this.loadData, this.update * 1000)
}else this.news.fail('First connection')
},
makeAuth() {
this.catchEmit(axios.get(`https://${this.newServer}/index.php/apps/news/api/v1-2/folders`, { this.catchEmit(axios.get(`https://${this.newServer}/index.php/apps/news/api/v1-2/folders`, {
headers: { Authorization: 'Basic ' + btoa(this.newUsername + ':' + this.newToken) }, headers: { Authorization: 'Basic ' + btoa(this.newUsername + ':' + this.newToken) },
timeout: this.timeout timeout: this.timeout
})).then(() => this.saveOptions({ ...this.$props, })).then(() => {
server: this.newServer, token: this.newToken, username: this.newUsername })) this.saveOptions({ ...this.$props,
server: this.newServer, token: this.newToken, username: this.newUsername })
this.init()
})
} }
}, },
created() { created() {
if(this.server) { this.init()
this.loadData()
if(this.update > 0)
setInterval(this.loadData, this.update * 1000)
}
} }
} }
</script> </script>

View File

@ -3,40 +3,40 @@
service-header(:emit="emit") service-header(:emit="emit")
template(#title) OpenWeatherMap template(#title) OpenWeatherMap
template(#settings) template(#settings)
setting-string(:id="'token'" :title="'Token'" :value="token" @change="saveOptionCouple")
setting-int(:id="'update'" :title="'Update interval'" :value="update" @change="saveOptionCouple") setting-int(:id="'update'" :title="'Update interval'" :value="update" @change="saveOptionCouple")
setting-int(:id="'forecastLimit'" :title="'Forecast limit'" :value="forecastLimit" @change="saveOptionCouple") setting-int(:id="'forecastLimit'" :title="'Forecast limit'" :value="forecastLimit" @change="saveOptionCouple")
p.setting p.setting
button(@click="showAdd = true") Add city button(@click="showAdd = true") Add city
template(v-if="weathers.length > 0 || cities.length == 0") loadable-block(:loadable="weathers")
.list template(#success)
.weather(v-for="(city, id) in weathers" :class="{ selected: selected == id }" @click.stop.prevent="makeSelect(id)") .list
.main(v-for="main in city.weather") weather(v-for="(city, id) in weathers.get()" :key="id" :selected="selectedId == id"
p {{ main.description }} :city="city" @select="makeSelect(id)" @remove="removeCity(id)")
.ic input.weather(v-show="showAdd" placeholder="city id" @keyup.enter="addCity(parseInt($event.target.value))")
img(:src="`https://openweathermap.org/img/w/${main.icon}.png`" :alt="main.main") loadable-block(:loadable="forecast").forecast
span.remove(@click.stop.prevent="removeCity(id)") template(#success)
.header chart.chart(:chartData="forecastChart")
| {{ city.name }}&nbsp; template(#error)
img.icon(:src="`https://openweathermap.org/images/flags/${city.sys.country.toLowerCase()}.png`" :alt="city.sys.country" :title="city.sys.country") form(@submit.prevent="makeAuth")
.data p
| {{ city.main.temp }}°C {{ city.main.humidity }}% label(for="token") Token:
input.weather(v-show="showAdd" placeholder="city id" @keyup.enter="addCity(parseInt($event.target.value))") input#token(v-model="newToken" required)
.forecast p
chart.chart(v-if="forecast" :chartData="forecastChart") input(type="submit" value="Connect")
.service-loader(v-else)
.service-loader(v-else)
</template> </template>
<script> <script>
import baseServiceVue from '../core/baseService.vue' import baseServiceVue from '../core/baseService.vue'
import Loadable from '../core/loadable/Loadable'
import chartVue from './chart.vue' import chartVue from './chart.vue'
import weatherVue from './weather.vue'
export default { export default {
name: 'openweathermap', name: 'openweathermap',
extends: baseServiceVue, extends: baseServiceVue,
components: { components: {
weather: weatherVue,
chart: chartVue chart: chartVue
}, },
props: { props: {
@ -73,9 +73,10 @@ export default {
}, },
timeout: this.timeout timeout: this.timeout
}), }),
weathers: [], newToken: this.token,
forecast: null, weathers: new Loadable(),
selected: 0, forecast: new Loadable(),
selectedId: 0,
showAdd: this.cities.length == 0 showAdd: this.cities.length == 0
}; };
}, },
@ -88,7 +89,7 @@ export default {
borderColor: 'white', borderColor: 'white',
borderWidth: 1, borderWidth: 1,
fill: false, fill: false,
data: this.forecast.map(function (line) { return { data: this.forecast.get().map(function (line) { return {
x: line.dt * 1000, y: line.main.temp x: line.dt * 1000, y: line.main.temp
} }) } })
},{ },{
@ -98,37 +99,42 @@ export default {
borderColor: '#DDDDDD', borderColor: '#DDDDDD',
backgroundColor: '#DDDDDD33', backgroundColor: '#DDDDDD33',
borderWidth: 1, borderWidth: 1,
data: this.forecast.filter(f => 'rain' in f && '3h' in f.rain).map(function (line) { return { data: this.forecast.get().filter(f => 'rain' in f && '3h' in f.rain).map(function (line) { return {
x: line.dt * 1000, y: line.rain['3h'] x: line.dt * 1000, y: line.rain['3h']
} }) } })
}] }]
} } } },
selected() {
return this.weathers.isSuccess() ? this.weathers.get()[this.selectedId] : null
},
hasWeathers() {
return this.weathers.isSuccess() && this.weathers.get().length > 0
}
}, },
methods: { methods: {
makeSelect(id) { makeSelect(id) {
this.selected = id this.selectedId = id
this.forecast = null
this.loadForecast() this.loadForecast()
}, },
updateData() { updateData() {
for (let i = 0; i < this.weathers.length; i++) { Lists.for(this.weathers.get(),
const weather = this.weathers[i]; (weather, i) => this.getWeather({ id: weather.id })
this.getWeather({ id: weather.id }) .then(res => this.$set(this.weathers.get(), i, res.data))
.then(res => this.$set(this.weathers, i, res.data)) )
}
this.loadForecast() this.loadForecast()
}, },
getWeather(params) { getWeather(params) {
return this.catchEmit(this.rest.get('weather', { params: params })) return this.catchEmit(this.rest.get('weather', { params: params }))
}, },
loadForecast() { loadForecast() {
if(this.weathers[this.selected]) { if(this.selected) {
this.catchEmit(this.rest.get('forecast', { params: { this.forecast.load(
id: this.weathers[this.selected].id, this.catchEmit(this.rest.get('forecast', { params: {
cnt: this.forecastLimit id: this.selected.id, cnt: this.forecastLimit
}})) }})),
.then(res => this.forecast = res.data.list) res => res.data.list
} )
} else this.forecast.fail('Any selection')
}, },
formatDate(dt) { formatDate(dt) {
const date = new Date(dt * 1000) const date = new Date(dt * 1000)
@ -139,19 +145,35 @@ export default {
this.saveOption('cities', this.cities) this.saveOption('cities', this.cities)
}, },
removeCity(key) { removeCity(key) {
this.cities.splice(key, 1) Lists.removeAt(this.cities, key)
this.saveOption('cities', this.cities) this.saveOption('cities', this.cities)
},
init() {
if(this.token) {
if(this.cities.length > 0) {
axios.all(this.cities.map(city => this.getWeather(city)))
.then(axios.spread((...ress) =>
this.weathers.success(ress.map(r => r.data))))
.then(this.loadForecast)
.catch(this.weathers.fail)
if(this.update > 0)
setInterval(this.updateData, this.update * 1000)
} else this.weathers.success([])
} else this.weathers.fail('First connection')
},
makeAuth() {
this.catchEmit(axios.get('https://api.openweathermap.org/data/2.5/weather', {
params: { q: 'London', appid: this.newToken },
timeout: this.timeout
})).then(() => {
this.saveOption('token', this.newToken)
this.init()
})
} }
}, },
created() { created() {
axios.all( this.init()
this.cities.map(
city => this.getWeather(city)
.then(res => this.weathers.push(res.data))))
.then(this.loadForecast)
if(this.update > 0)
setInterval(this.updateData, this.update * 1000)
} }
} }
</script> </script>

View File

@ -0,0 +1,22 @@
<template lang="pug">
.weather(:class="{ selected: selected }" @click.stop.prevent="$emit('select')")
.main(v-for="main in city.weather")
p {{ main.description }}
.ic
img(:src="`https://openweathermap.org/img/w/${main.icon}.png`" :alt="main.main")
span.remove(@click.stop.prevent="$emit('remove')")
.header
| {{ city.name }}&nbsp;
img.icon(:src="`https://openweathermap.org/images/flags/${city.sys.country.toLowerCase()}.png`" :alt="city.sys.country" :title="city.sys.country")
.data
| {{ city.main.temp }}°C {{ city.main.humidity }}%
</template>
<script>
export default {
props: {
city: Object,
selected: Boolean
}
}
</script>

View File

@ -13,6 +13,7 @@
<script src="compiler/dist/mastodon/main.js"></script> <script src="compiler/dist/mastodon/main.js"></script>
<script src="compiler/dist/openweathermap/main.js"></script> <script src="compiler/dist/openweathermap/main.js"></script>
<script src="compiler/dist/nextcloud-news/main.js"></script> <script src="compiler/dist/nextcloud-news/main.js"></script>
<script src="compiler/dist/discord/main.js"></script>
</head> </head>
<body> <body>
<noscript>Mixit needs javascript enabled to work correctly. Sorry</noscript> <noscript>Mixit needs javascript enabled to work correctly. Sorry</noscript>

View File

@ -75,6 +75,13 @@ input, select, button {
#services > div { #services > div {
overflow: auto; overflow: auto;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
} }
#services > div .service-header .title, #services > div .service-header .settings { #services > div .service-header .title, #services > div .service-header .settings {
@ -95,6 +102,10 @@ input, select, button {
width: 1.2em; width: 1.2em;
} }
#services > div .service-content {
overflow: hidden;
}
#services .service-loader { #services .service-loader {
display: inline-block; display: inline-block;
width: 64px; width: 64px;
@ -136,20 +147,11 @@ input, select, button {
} }
} }
.mastodon {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
}
.mastodon .client { .mastodon .client {
display: -webkit-box; display: -webkit-box;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
height: 100%;
overflow-y: auto; overflow-y: auto;
} }
@ -251,10 +253,14 @@ input, select, button {
float: right; float: right;
} }
.openweathermap { .openweathermap .loadable-block {
overflow: hidden;
display: -webkit-box; display: -webkit-box;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-box-direction: normal; -webkit-box-direction: normal;
-ms-flex-direction: column; -ms-flex-direction: column;
@ -284,6 +290,7 @@ input, select, button {
-ms-flex: 1; -ms-flex: 1;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
height: 100%;
} }
.openweathermap .forecast .chart { .openweathermap .forecast .chart {
@ -357,16 +364,6 @@ input, select, button {
margin-top: -10px; margin-top: -10px;
} }
.nextcloud-news {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
}
.nextcloud-news .unreaded { .nextcloud-news .unreaded {
overflow-y: auto; overflow-y: auto;
} }

File diff suppressed because one or more lines are too long

View File

@ -70,6 +70,8 @@ input, select, button
justify-items: stretch justify-items: stretch
& > div & > div
overflow: auto overflow: auto
display: flex
flex-direction: column
.service-header .service-header
.title, .settings .title, .settings
@include tile @include tile
@ -80,6 +82,8 @@ input, select, button
.settings .position .settings .position
float: right float: right
width: 1.2em width: 1.2em
.service-content
overflow: hidden
.service-loader .service-loader
display: inline-block display: inline-block
width: 64px width: 64px
@ -101,10 +105,9 @@ input, select, button
transform: rotate(360deg) transform: rotate(360deg)
.mastodon .mastodon
display: flex
flex-direction: column
.client .client
display: flex display: flex
height: 100%
overflow-y: auto overflow-y: auto
.header .header
@include tile @include tile
@ -167,8 +170,11 @@ input, select, button
float: right float: right
.openweathermap .openweathermap
display: flex .loadable-block
flex-direction: column overflow: hidden
display: flex
flex: 1
flex-direction: column
.list .list
display: flex display: flex
flex-wrap: wrap flex-wrap: wrap
@ -178,6 +184,7 @@ input, select, button
.forecast .forecast
flex: 1 flex: 1
overflow: hidden overflow: hidden
height: 100%
.chart .chart
position: relative position: relative
height: 100% height: 100%
@ -216,8 +223,6 @@ input, select, button
margin-top: -10px margin-top: -10px
.nextcloud-news .nextcloud-news
display: flex
flex-direction: column
.unreaded .unreaded
overflow-y: auto overflow-y: auto
.news .news