Loadable, more auth and starting discord
This commit is contained in:
parent
edc9a87db8
commit
67e6379e41
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,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)
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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)") 👁
|
||||
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)") 👁
|
||||
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>
|
|
@ -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 }}
|
||||
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>
|
|
@ -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 }}
|
||||
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>
|
|
@ -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>
|
||||
|
|
39
main.css
39
main.css
|
@ -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
17
main.sass
17
main.sass
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue