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

View File

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

View File

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

View File

@ -3,40 +3,40 @@
service-header(:emit="emit")
template(#title) OpenWeatherMap
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="'forecastLimit'" :title="'Forecast limit'" :value="forecastLimit" @change="saveOptionCouple")
p.setting
button(@click="showAdd = true") Add city
template(v-if="weathers.length > 0 || cities.length == 0")
.list
.weather(v-for="(city, id) in weathers" :class="{ selected: selected == id }" @click.stop.prevent="makeSelect(id)")
.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="removeCity(id)")
.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 }}%
input.weather(v-show="showAdd" placeholder="city id" @keyup.enter="addCity(parseInt($event.target.value))")
.forecast
chart.chart(v-if="forecast" :chartData="forecastChart")
.service-loader(v-else)
.service-loader(v-else)
loadable-block(:loadable="weathers")
template(#success)
.list
weather(v-for="(city, id) in weathers.get()" :key="id" :selected="selectedId == id"
:city="city" @select="makeSelect(id)" @remove="removeCity(id)")
input.weather(v-show="showAdd" placeholder="city id" @keyup.enter="addCity(parseInt($event.target.value))")
loadable-block(:loadable="forecast").forecast
template(#success)
chart.chart(:chartData="forecastChart")
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 from '../core/baseService.vue'
import Loadable from '../core/loadable/Loadable'
import chartVue from './chart.vue'
import weatherVue from './weather.vue'
export default {
name: 'openweathermap',
extends: baseServiceVue,
components: {
weather: weatherVue,
chart: chartVue
},
props: {
@ -73,9 +73,10 @@ export default {
},
timeout: this.timeout
}),
weathers: [],
forecast: null,
selected: 0,
newToken: this.token,
weathers: new Loadable(),
forecast: new Loadable(),
selectedId: 0,
showAdd: this.cities.length == 0
};
},
@ -88,7 +89,7 @@ export default {
borderColor: 'white',
borderWidth: 1,
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
} })
},{
@ -98,37 +99,42 @@ export default {
borderColor: '#DDDDDD',
backgroundColor: '#DDDDDD33',
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']
} })
}]
} }
} },
selected() {
return this.weathers.isSuccess() ? this.weathers.get()[this.selectedId] : null
},
hasWeathers() {
return this.weathers.isSuccess() && this.weathers.get().length > 0
}
},
methods: {
makeSelect(id) {
this.selected = id
this.forecast = null
this.selectedId = id
this.loadForecast()
},
updateData() {
for (let i = 0; i < this.weathers.length; i++) {
const weather = this.weathers[i];
this.getWeather({ id: weather.id })
.then(res => this.$set(this.weathers, i, res.data))
}
Lists.for(this.weathers.get(),
(weather, i) => this.getWeather({ id: weather.id })
.then(res => this.$set(this.weathers.get(), i, res.data))
)
this.loadForecast()
},
getWeather(params) {
return this.catchEmit(this.rest.get('weather', { params: params }))
},
loadForecast() {
if(this.weathers[this.selected]) {
this.catchEmit(this.rest.get('forecast', { params: {
id: this.weathers[this.selected].id,
cnt: this.forecastLimit
}}))
.then(res => this.forecast = res.data.list)
}
if(this.selected) {
this.forecast.load(
this.catchEmit(this.rest.get('forecast', { params: {
id: this.selected.id, cnt: this.forecastLimit
}})),
res => res.data.list
)
} else this.forecast.fail('Any selection')
},
formatDate(dt) {
const date = new Date(dt * 1000)
@ -139,19 +145,35 @@ export default {
this.saveOption('cities', this.cities)
},
removeCity(key) {
this.cities.splice(key, 1)
Lists.removeAt(this.cities, key)
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() {
axios.all(
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)
this.init()
}
}
</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/openweathermap/main.js"></script>
<script src="compiler/dist/nextcloud-news/main.js"></script>
<script src="compiler/dist/discord/main.js"></script>
</head>
<body>
<noscript>Mixit needs javascript enabled to work correctly. Sorry</noscript>

View File

@ -75,6 +75,13 @@ input, select, button {
#services > div {
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 {
@ -95,6 +102,10 @@ input, select, button {
width: 1.2em;
}
#services > div .service-content {
overflow: hidden;
}
#services .service-loader {
display: inline-block;
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 {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
height: 100%;
overflow-y: auto;
}
@ -251,10 +253,14 @@ input, select, button {
float: right;
}
.openweathermap {
.openweathermap .loadable-block {
overflow: hidden;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
@ -284,6 +290,7 @@ input, select, button {
-ms-flex: 1;
flex: 1;
overflow: hidden;
height: 100%;
}
.openweathermap .forecast .chart {
@ -357,16 +364,6 @@ input, select, button {
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 {
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
& > div
overflow: auto
display: flex
flex-direction: column
.service-header
.title, .settings
@include tile
@ -80,6 +82,8 @@ input, select, button
.settings .position
float: right
width: 1.2em
.service-content
overflow: hidden
.service-loader
display: inline-block
width: 64px
@ -101,10 +105,9 @@ input, select, button
transform: rotate(360deg)
.mastodon
display: flex
flex-direction: column
.client
display: flex
height: 100%
overflow-y: auto
.header
@include tile
@ -167,8 +170,11 @@ input, select, button
float: right
.openweathermap
display: flex
flex-direction: column
.loadable-block
overflow: hidden
display: flex
flex: 1
flex-direction: column
.list
display: flex
flex-wrap: wrap
@ -178,6 +184,7 @@ input, select, button
.forecast
flex: 1
overflow: hidden
height: 100%
.chart
position: relative
height: 100%
@ -216,8 +223,6 @@ input, select, button
margin-top: -10px
.nextcloud-news
display: flex
flex-direction: column
.unreaded
overflow-y: auto
.news