Add layouts and more

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,35 @@
<script>
import Loadable from './loadable/Loadable.js'
import connectedServiceVue from './connectedService.vue'
export default {
extends: connectedServiceVue,
data() {
return {
account: new Loadable()
}
},
computed: {
connector() {
return this.account
}
},
methods: {
load() {
this.account.load(this.catchEmit(this.getAccount(this.auth)), this.mapAccount)
},
checkAuth(auth) {
return this.getAccount(auth)
},
getAccount() {
this.mustDefine('getAccount(auth) method')
},
mapAccount(res) {
return res.data
},
mapServiceName(res) {
return `${this.serviceName} ${this.mapAccount(res)}`
}
}
}
</script>

View File

@ -22,7 +22,38 @@ export default {
settingInt: settingIntVue,
settingString: settingStringVue
},
extends: serviceEmiterVue
extends: serviceEmiterVue,
props: {
auth: {
type: Object,
default: () => ({})
},
options: {
type: Object,
default: () => ({})
}
},
data() {
return {
newAuth: {}
}
},
watch: {
auth() {
this.init()
}
},
created() {
this.init()
},
methods: {
init() {
this.mustDefine('init() method')
},
mustDefine(name) {
this.emitError('Must define ' + name)
}
}
}
export const Loadable = _Loadable

View File

@ -0,0 +1,42 @@
<script>
import baseServiceVue from './baseService.vue'
import Loadable from './loadable/Loadable'
export default {
extends: baseServiceVue,
computed: {
isSetup() {
this.mustDefine('isSetup computed')
return false
},
connector() {
this.mustDefine('connector computed')
return new Loadable()
},
serviceName() {
this.mustDefine('serviceName computed')
return undefined
}
},
methods: {
init() {
if(this.isSetup) {
this.load()
} else this.connector.fail('First connection')
},
makeAuth() {
this.catchEmit(this.checkAuth(this.newAuth)).then(res =>
this.saveService(this.mapServiceName(res, this.newAuth), this.newAuth))
},
load() {
this.mustDefine('load() method')
},
checkAuth() {
this.mustDefine('checkAuth(auth) method')
},
mapServiceName() {
return this.serviceName
},
}
}
</script>

View File

@ -16,7 +16,7 @@ export default {
mixins: [ timedMixin ],
props: {
date: {
type: Date,
type: [Date, Number, String],
default: Date.now
}
},

View File

@ -17,7 +17,6 @@ export default class {
this.loaded = false
this.data = undefined
this.error = undefined
this.loadingMore = false
}
success(data) {
this.loaded = true
@ -39,12 +38,4 @@ export default class {
throw err
})
}
loadMore(promise, then) {
this.loadingMore = true
promise.then(res => {
then(res, this.data, this)
this.loadingMore = false
})
}
}

View File

@ -0,0 +1,16 @@
import Loadable from './Loadable'
export default class extends Loadable {
reset() {
super.reset()
this.loadingMore = false
}
loadMore(promise, then) {
this.loadingMore = true
promise.then(res => {
then(res, this.data, this)
this.loadingMore = false
})
}
}

View File

@ -11,10 +11,7 @@ export default {
this.emit('error', err)
},
saveOptions(options) {
this.emit('saveAll', options)
this.$nextTick(function(){
this.$forceUpdate()
})
this.emit('saveOptions', options)
},
saveOption(key, value) {
this.saveOptionCouple({
@ -22,9 +19,11 @@ export default {
})
},
saveOptionCouple(couple) {
this.emit('save', couple)
this.$nextTick(function(){
this.$forceUpdate()
this.emit('saveOption', couple)
},
saveService(name, auth) {
this.emit('saveService', {
name: name, auth: auth
})
},
catchEmit(req) {

View File

@ -8,6 +8,7 @@
@keyup.left.ctrl.exact="onMove('y', -1)" @keyup.right.ctrl.exact="onMove('y', 1)"
@keyup.up.alt.exact="onMove('h', -1)" @keyup.down.alt.exact="onMove('h', 1)"
@keyup.left.alt.exact="onMove('w', -1)" @keyup.right.alt.exact="onMove('w', 1)"
@keyup.delete.ctrl.exact="onRemove" @keyup.delete.alt.exact="onRemoveService"
)
slot(name="settings")
</template>
@ -27,6 +28,12 @@ export default {
onMove(type, direction) {
this.emit('move', { type: type, direction: direction })
},
onRemove() {
this.emit('remove', { })
},
onRemoveService() {
this.emit('removeService', { })
}
}
}
</script>

View File

@ -2,7 +2,7 @@
.client(@scroll.passive="onScroll")
loadable-block.list(:loadable="guilds")
template(#success)
guild(v-for="guild in guilds.get()" :key="guild.id" :guild="guild" :showMedia="showMedia")
guild(v-for="guild in guilds.get()" :key="guild.id" :guild="guild" :showMedia="options.showMedia")
</template>
<script>
@ -22,33 +22,18 @@ export default {
extends: serviceEmiterVue,
mixins: [ timerMinin ],
props: {
token: {
default: undefined,
type: String
auth: {
type: Object,
default: () => ({})
},
timeout: {
default: 5000,
type: Number
},
reconnect: {
default: false,
type: Boolean
},
buffer: {
default: 20,
type: Number
},
showMedia: {
default: true,
type: Boolean
}
options: { type: Object, default: () => ({}) }
},
data() {
return {
rest: axios.create({
baseURL: 'https://discordapp.com/api/',
headers: { Authorization: this.token },
timeout: this.timeout
headers: { Authorization: this.auth.token },
timeout: this.options.timeout
}),
guilds: new Loadable()
}
@ -62,7 +47,7 @@ export default {
},
methods: {
get(path, options = {}) {
return this.catchEmit(this.rest.get(path, { params: { limit: this.buffer, ...options } }))
return this.catchEmit(this.rest.get(path, { params: { limit: this.options.buffer, ...options } }))
},
onScroll() {
/*if(!this.loadingOlder && event.target.scrollHeight - event.target.clientHeight - event.target.scrollTop - 100 < 0) {

View File

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

View File

@ -5,7 +5,7 @@
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-if="showStatus(status)" :key="status.id" :status="status" :now="now" :showMedia="options.showMedia" @mark="onStatusMark")
.status(v-show="statues.loadingMore")
.service-loader
.notifications(v-if="hasNotifications")
@ -14,7 +14,7 @@
span.date(@click.stop.prevent="onNotificationsClear")
.list
notification(v-for="notification in notifications.get()" :key="notification.id" :notification="notification" :now="now"
:showMedia="showMedia" @dismiss="onNotificationDismiss" @mark="onStatusMark")
:showMedia="options.showMedia" @dismiss="onNotificationDismiss" @mark="onStatusMark")
</template>
<script>
@ -28,6 +28,7 @@ import statusVue from './status.vue'
import notificationVue from './notification.vue'
import Loadable from '../core/loadable/Loadable'
import ReLoadable from '../core/loadable/ReLoadable'
import loadableBlockVue from '../core/loadable/loadableBlock.vue'
export default {
@ -39,35 +40,20 @@ export default {
extends: serviceEmiterVue,
mixins: [ timerMinin ],
props: {
server: {
type: String,
default: undefined
auth: {
type: Object,
default: () => ({})
},
token: {
type: String,
default: undefined
},
timeout: {
type: Number,
default: 5000
},
reconnect: Boolean,
buffer: {
type: Number,
default: 20
},
reblog: Boolean,
reply: Boolean,
showMedia: Boolean
options: { type: Object, default: () => ({}) }
},
data() {
return {
rest: axios.create({
baseURL: `https://${this.server}/api/v1/`,
headers: { Authorization: 'Bearer ' + this.token },
timeout: this.timeout
baseURL: `https://${this.auth.server}/api/v1/`,
headers: { Authorization: 'Bearer ' + this.auth.token },
timeout: this.options.timeout
}),
statues: new Loadable(),
statues: new ReLoadable(),
notifications: new Loadable()
}
},
@ -89,7 +75,7 @@ export default {
},
methods: {
get(path, options = {}) {
return this.catchEmit(this.rest.get(path, { params: { limit: this.buffer, ...options } }))
return this.catchEmit(this.rest.get(path, { params: { limit: this.options.buffer, ...options } }))
},
post(path, options = {}) {
return this.catchEmit(this.rest.post(path, options))
@ -104,14 +90,14 @@ export default {
(res, statues) => Lists.pushAll(statues, res.data)
)
} else if(event.target.scrollTop < 20) {
this.statues.get().splice(this.buffer)
this.statues.get().splice(this.options.buffer)
}
},
removeById(ls, id) {
Lists.removeFirst(ls, e => e.id === id)
},
showStatus(status) {
return (!status.in_reply_to_id || this.reply) && (!status.reblog || this.reblog)
return (!status.in_reply_to_id || this.options.reply) && (!status.reblog || this.options.reblog)
},
onStatusMark(action) {
this.post(`/statuses/${action.id}/${action.type}`)
@ -127,7 +113,7 @@ export default {
},
setupStream() {
const ws = new WebSocket(
`wss://${this.server}/api/v1/streaming?access_token=${this.token}&stream=user`
`wss://${this.auth.server}/api/v1/streaming?access_token=${this.auth.token}&stream=user`
)
ws.onmessage = event => {
event = JSON.parse(event.data)
@ -150,9 +136,9 @@ export default {
ws.onclose = () => {
this.emitError(
'Mastodon stream disconnected !' +
(this.reconnect ? ' Reconnecting...' : '')
(this.options.reconnect ? ' Reconnecting...' : '')
)
if (this.reconnect) setTimeout(() => this.setupStream(), this.timeout)
if (this.options.reconnect) setTimeout(() => this.setupStream(), this.options.timeout)
}
}
}

View File

@ -2,113 +2,64 @@
.mastodon
service-header(:emit="emit")
template(#title)
| Mastodon:
| {{ serviceName }}:
loadable-inline(:loadable="account")
template(#success)
span(v-html="parseEmojis(account.data.display_name, account.data.emojis) + '@' + server")
span(v-html="parseEmojis(account.data.display_name, account.data.emojis) + '@' + auth.server")
template(#settings)
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")
setting-boolean(:id="'reconnect'" :title="'Reconnect'" :value="params.reconnect" @change="saveOptionCouple")
setting-boolean(:id="'reblog'" :title="'Show reblogs'" :value="params.reblog" @change="saveOptionCouple")
setting-boolean(:id="'reply'" :title="'Show replies'" :value="params.reply" @change="saveOptionCouple")
setting-int(:id="'buffer'" :title="'Buffer size'" :value="params.buffer" @change="saveOptionCouple")
setting-boolean(:id="'showMedia'" :title="'Show medias'" :value="params.showMedia" @change="saveOptionCouple")
loadable-block.service-content(:loadable="account")
template(#success)
client(v-bind="$props")
client(:auth="auth" :options="params" :emit="emit")
template(#error)
form(@submit.prevent="makeAuth")
p
label(for="server") Server:
input#server(v-model="newServer" required)
input#server(v-model="newAuth.server" required)
p
label(for="token") Token:
input#token(v-model="newToken" required)
input#token(v-model="newAuth.token" required)
p
input(type="submit" value="Connect")
</template>
<script>
/* global axios */
import baseServiceVue, { Loadable } from '../core/baseService.vue'
import accountServiceVue from '../core/accountService.vue'
import { parseEmojisMixin } from './tools'
import clientVue from './client.vue'
export default { //TODO: Use oauth
name: 'Mastodon',
components: {
client: clientVue
},
extends: baseServiceVue,
components: { client: clientVue },
extends: accountServiceVue,
mixins: [ parseEmojisMixin ],
props: {
server: {
type: String,
default: undefined
computed: {
params() {
return { timeout: 5000, reconnect: false, buffer: 20, reblog: true, reply: false,
showMedia: true, ...this.options }
},
token: {
type: String,
default: undefined
isSetup() {
return this.auth && this.auth.server && this.auth.token
},
timeout: {
default: 5000,
type: Number
},
reconnect: {
default: false,
type: Boolean
},
buffer: {
default: 20,
type: Number
},
reblog: {
default: true,
type: Boolean
},
reply: {
default: false,
type: Boolean
},
showMedia: {
default: true,
type: Boolean
serviceName() {
return 'Mastodon'
}
},
data() {
return {
newServer: this.server,
newToken: this.token,
account: new Loadable()
}
},
created() {
this.init()
},
methods: {
getMe(server, token) {
return this.catchEmit(axios.get(`https://${server}/api/v1/accounts/verify_credentials`, {
getAccount({ server, token }) {
return axios.get(`https://${server}/api/v1/accounts/verify_credentials`, {
headers: { Authorization: 'Bearer ' + token },
timeout: this.timeout
}))
timeout: this.params.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 })
this.init()
})
mapServiceName(res, { server }) {
return `${this.serviceName} ${this.mapAccount(res).acct}@${server}`
}
}
}

View File

@ -1,13 +1,13 @@
<template lang="pug">
.nextcloud-news(v-show="showEmpty || hasNews || !isSetup")
.nextcloud-news(v-show="showService")
service-header(:emit="emit")
template(#title)
| Nextcloud News
| {{ serviceName }}
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")
setting-int(:id="'update'" :title="'Update interval'" :value="params.update" @change="saveOptionCouple")
setting-int(:id="'buffer'" :title="'Buffer size'" :value="params.buffer" @change="saveOptionCouple")
setting-boolean(:id="'showEmpty'" :title="'Show empty'" :value="params.showEmpty" @change="saveOptionCouple")
loadable-block.unreaded(:loadable="news")
template(#success)
.news(v-for="line in news.get()")
@ -20,20 +20,20 @@
form(@submit.prevent="makeAuth")
p
label(for="server") Server:
input#server(v-model="newServer" required)
input#server(v-model="newAuth.server" required)
p
label(for="username") Username:
input#username(v-model="newUsername" required)
input#username(v-model="newAuth.username" required)
p
label(for="token") Token:
input#token(v-model="newToken" required)
input#token(v-model="newAuth.token" required)
p
input(type="submit" value="Connect")
</template>
<script>
/* global axios */
import baseServiceVue from '../core/baseService.vue'
import connectedServiceVue from '../core/connectedService.vue'
import fromNowVue, { timerMinin } from '../core/fromNow.vue'
import Loadable from '../core/loadable/Loadable'
@ -44,68 +44,38 @@ export default {
components: {
fromNow: fromNowVue
},
extends: baseServiceVue,
extends: connectedServiceVue,
mixins: [ timerMinin ],
props: {
server: {
type: String,
default: undefined
},
username: {
type: String,
default: undefined
},
token: {
type: String,
default: undefined
},
timeout: {
default: 5000,
type: Number
},
buffer : {
default: -1,
type: Number
},
update: {
default: 5 * 60, //5min
type: Number
},
showEmpty: {
default: false,
type: Boolean
}
},
data() {
return {
rest: axios.create({
baseURL: `https://${this.server}/index.php/apps/news/api/v1-2/`,
timeout: this.timeout,
headers: {
Authorization: 'Basic ' + btoa(this.username + ':' + this.token)
}
}),
news: new Loadable(),
newServer: this.server,
newUsername: this.username,
newToken: this.token,
rest: undefined, //NOTE: set in this.init()
news: new Loadable()
}
},
computed: {
params() {
return { timeout: 5000, buffer: -1, update: 5 * 60, showEmpty: true, ...this.options }
},
isSetup() {
return this.auth && this.auth.server && this.auth.username && this.auth.token
},
connector() {
return this.news
},
serviceName() {
return 'Nextcloud News'
},
hasNews() {
return this.news.isSuccess() && this.news.get().length > 0
},
isSetup() {
return this.server && this.username && this.token
showService() {
return this.params.showEmpty || this.hasNews || !this.isSetup
}
},
created() {
this.init()
},
methods: {
loadData() {
this.news.load(
this.catchEmit(this.rest.get('/items', { params: { batchSize: this.buffer, type: 3, getRead: false } })),
this.catchEmit(this.rest.get('/items', { params: { batchSize: this.params.buffer, type: 3, getRead: false } })),
res => res.data.items.map(n => {
n.open = false
return n
@ -119,22 +89,24 @@ export default {
this.catchEmit(this.rest.put(`/items/${id}/read`))
.then(() => this.removeNews(id))
},
init() {
if(this.isSetup) {
this.loadData()
load() {
this.rest = axios.create({
baseURL: `https://${this.auth.server}/index.php/apps/news/api/v1-2/`,
timeout: this.params.timeout,
headers: {
Authorization: 'Basic ' + btoa(this.auth.username + ':' + this.auth.token)
}
}) //NOTE: required by this.params
if(this.update > 0)
setInterval(this.loadData, this.update * 1000)
}else this.news.fail('First connection')
this.loadData()
if(this.params.update > 0)
setInterval(this.loadData, this.params.update * 1000)
},
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 })
this.init()
checkAuth({ server, username, token }){
return axios.get(`https://${server}/index.php/apps/news/api/v1-2/folders`, {
headers: { Authorization: 'Basic ' + btoa(username + ':' + token) },
timeout: this.params.timeout
})
}
}

View File

@ -1,10 +1,10 @@
<template lang="pug">
.openweathermap
service-header(:emit="emit")
template(#title) OpenWeatherMap
template(#title) {{ serviceName }}
template(#settings)
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="'update'" :title="'Update interval'" :value="params.update" @change="saveOptionCouple")
setting-int(:id="'forecastLimit'" :title="'Forecast limit'" :value="params.forecastLimit" @change="saveOptionCouple")
p.setting
button(@click="showAdd = true") Add city
loadable-block(:loadable="weathers")
@ -20,14 +20,14 @@
form(@submit.prevent="makeAuth")
p
label(for="token") Token:
input#token(v-model="newToken" required)
input#token(v-model="newAuth.token" required)
p
input(type="submit" value="Connect")
</template>
<script>
/* global axios */
import baseServiceVue from '../core/baseService.vue'
import connectedServiceVue from '../core/connectedService.vue'
import Lists from '../core/Lists.js'
import Loadable from '../core/loadable/Loadable'
@ -40,52 +40,30 @@ export default {
weather: weatherVue,
chart: chartVue
},
extends: baseServiceVue,
props: {
token: {
type: String,
default: undefined
},
cities: {
type: Array,
default: function () {
return []
}
},
timeout: {
default: 5000,
type: Number
},
update: {
default: 10 * 60, //10min
type: Number
},
lang: {
default: 'fr',
type: String
},
forecastLimit: {
default: 9,
type: Number
}
},
extends: connectedServiceVue,
data() {
return {
rest: axios.create({
baseURL: 'https://api.openweathermap.org/data/2.5/',
params: {
appid: this.token, units: 'metric', lang: this.lang
},
timeout: this.timeout
}),
newToken: this.token,
rest: undefined, //NOTE: set in this.init()
weathers: new Loadable(),
forecast: new Loadable(),
selectedId: 0,
showAdd: this.cities.length == 0
showAdd: false
}
},
computed: {
params() {
return { cities: [], timeout: 5000, update: 10 * 60, lang: 'fr',
forecastLimit: 9, ...this.options }
},
isSetup() {
return this.auth && this.auth.token
},
serviceName() {
return 'OpenWeatherMap'
},
connector() {
return this.weathers
},
forecastChart() { return {
datasets: [{
type: 'line',
@ -117,7 +95,7 @@ export default {
}
},
created() {
this.init()
this.$watch('options.cities', this.init)
},
methods: {
makeSelect(id) {
@ -138,7 +116,7 @@ export default {
if(this.selected) {
this.forecast.load(
this.catchEmit(this.rest.get('forecast', { params: {
id: this.selected.id, cnt: this.forecastLimit
id: this.selected.id, cnt: this.params.forecastLimit
}})),
res => res.data.list
)
@ -149,34 +127,38 @@ export default {
return `${date.toLocaleDateString()} ${date.getHours()}h`
},
addCity(id) {
this.cities.push({ id: id })
this.saveOption('cities', this.cities)
this.params.cities.push({ id: id })
this.saveOption('cities', this.params.cities)
},
removeCity(key) {
Lists.removeAt(this.cities, key)
this.saveOption('cities', this.cities)
Lists.removeAt(this.params.cities, key)
this.saveOption('cities', this.params.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)
load() {
this.rest = axios.create({
baseURL: 'https://api.openweathermap.org/data/2.5/',
params: {
appid: this.auth.token, units: 'metric', lang: this.params.lang
},
timeout: this.params.timeout
}) //NOTE: required by this.params
this.showAdd = this.params.cities.length == 0
if(this.update > 0)
setInterval(this.updateData, this.update * 1000)
} else this.weathers.success([])
} else this.weathers.fail('First connection')
if(this.params.cities.length > 0) {
axios.all(this.params.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.params.update * 1000)
} else this.weathers.success([])
},
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()
checkAuth({ token }) {
return axios.get('https://api.openweathermap.org/data/2.5/weather', {
params: { q: 'London', appid: token },
timeout: this.params.timeout
})
}
}

View File

@ -21,15 +21,35 @@
<div id="errors" v-show="errors">
<p class="error" v-for="(error, key) in errors" @click="removeError(key)">{{ error }}</p>
</div>
<div id="manager">
<button @click="showManager = !showManager">+</button>
<input v-show="showManager" v-model="newService" @keyup.enter="addService">
</div>
<div id="services">
<component
v-for="(service, key) in services" :is="service.type" :emit="makeEmiter(key)"
:key="key" v-bind="service.options" :style="gridPos(key, service.position)"
/>
<div id="content">
<div id="services">
<div class="tile" v-if="layout" v-for="tile in layoutTiles" :style="tile.grid">
<component :is="tile.service.type" :emit="tile.emiter" :auth="tile.service.auth" :options="tile.options" />
</div>
</div>
<button id="showManager" @click="showManager = !showManager">{{ showManager ? '▼' : '▲' }}</button>
<div id="manager" v-show="showManager">
<div>
<input v-model="newService" @keyup.enter="addService" placeholder="service">
</div>
<div id="layout-select">
<div class="layout" v-for="(layout, id) in layouts">
<template v-if="layoutId == id">
<input :value="layout.name" @keyup.ctrl.delete="removeSelectedLayout()"
@keyup.enter="renameSelectedLayout($event.target.value)">
</template>
<button v-else @click="layoutId = id">{{ layout.name }}</button>
</div>
<div><button @click="addLayout">+</button></div>
</div>
<div>
<select @change="showService($event.target.value)">
<option v-for="(service, key) in services.get()" :value="key">
{{ service.name || service.type }}
</option>
</select>
</div>
</div>
</div>
</div>
<script src="main.js"></script>

View File

@ -56,13 +56,46 @@ input, select, button {
padding: 0.3em;
}
#manager {
#content {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
height: 100vh;
}
#showManager {
position: absolute;
bottom: 0;
}
#manager {
background-color: #222;
border-radius: 0.3em;
padding-left: 1em;
height: 1.3em;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
#layout-select {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
#services {
height: 100vh;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
overflow: hidden;
display: -ms-grid;
display: grid;
grid-gap: .2em;
@ -73,8 +106,12 @@ input, select, button {
justify-items: stretch;
}
#services > div {
#services .tile {
overflow: auto;
}
#services .tile > div {
height: 100%;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
@ -84,25 +121,25 @@ input, select, button {
flex-direction: column;
}
#services > div .service-header .title, #services > div .service-header .settings {
#services .tile > div .service-header .title, #services .tile > div .service-header .settings {
margin: 0.3em;
background-color: #222;
border-radius: 0.3em;
padding: 0.3em;
}
#services > div .service-header .title {
#services .tile > div .service-header .title {
font-size: large;
text-align: center;
font-weight: bold;
}
#services > div .service-header .settings .position {
#services .tile > div .service-header .settings .position {
float: right;
width: 1.2em;
}
#services > div .service-content {
#services .tile > div .service-content {
overflow: hidden;
}

File diff suppressed because one or more lines are too long

178
main.js
View File

@ -1,31 +1,148 @@
/* globals Vue */
/* exported app */
const layoutsStorage = 'layouts'
const servicesStorage = 'services'
class WebStorageHandler { //TODO: extends loadable
constructor(storage, key, data) {
this.storage = storage
this.key = key
this.data = data || []
}
get() {
return this.data
}
load() {
if (this.storage.getItem(this.key)) {
try {
this.data = JSON.parse(this.storage.getItem(this.key))
} catch (e) {
this.storage.removeItem(this.key)
}
}
}
save() {
this.storage.setItem(this.key, JSON.stringify(this.data))
}
}
var app = new Vue({
el: '#app',
data: {
showManager: false,
layouts: new WebStorageHandler(window.localStorage, layoutsStorage, [{ name: 'main', tiles: [] }]),
layoutId: 0,
services: new WebStorageHandler(window.localStorage, servicesStorage),
newService: '',
services: [],
errors: [],
bus: new Vue()
},
mounted() {
if (localStorage.getItem(servicesStorage)) { //TODO: allow external storage
try {
this.services = JSON.parse(localStorage.getItem(servicesStorage))
} catch (e) {
localStorage.removeItem(servicesStorage)
}
}
this.layouts.load()
this.services.load()
this.bus.$on('error', this.onError)
this.bus.$on('saveAll', this.onSaveAll)
this.bus.$on('save', this.onSave)
this.bus.$on('saveOptions', this.onSaveOptions)
this.bus.$on('saveOption', this.onSaveOption)
this.bus.$on('move', this.onMove)
this.bus.$on('remove', this.onRemove)
this.bus.$on('saveService', this.onSaveService)
this.bus.$on('removeService', this.onRemoveService)
},
computed: {
layout() {
return this.layouts.get()[this.layoutId]
},
layoutTiles() {
return this.layout.tiles.map((tile, key) => ({
...tile, service: this.loadService(key, tile.service),
grid: this.gridPos(tile.position), emiter: this.makeEmiter(key)
}))
}
},
methods: {
//Layouts
addLayout() {
this.layouts.get().push({
name: 'layout' + this.layouts.get().length, tiles: []
})
this.layouts.save()
},
renameSelectedLayout(name) {
this.layout.name = name
this.layouts.save()
},
removeSelectedLayout() {
this.layouts.get().splice(this.layoutId, 1)
this.layoutId = 0
this.layouts.save()
},
//Tiles
showService(id) {
this.layout.tiles.push({
service: id, position: {}, options: {}
})
this.layouts.save()
},
tile(id) {
return this.layout.tiles[id]
},
onSaveOption({ key, msg }) {
this.$set(this.tile(key).options, msg.key, msg.value)
this.layouts.save()
},
onSaveOptions({ key, msg }) {
this.tile(key).options = Object.assign({}, this.tile(key).options, msg)
this.layouts.save()
},
onMove({ key, msg }) {
this.$set(this.tile(key).position, msg.type, Math.max(1,
(this.tile(key).position[msg.type] || 1) + msg.direction
))
this.layouts.save()
},
onRemove({ key }) {
this.layout.tiles.splice(key, 1)
this.layouts.save()
},
//Services
loadService(key, id) {
const ser = this.services.get()[id]
if (ser)
return ser
else {
this.onRemove({ key })
this.addError('Removing missing service')
}
},
addService() {
if (this.newService) {
this.services.get().push({ type: this.newService, name: this.newService, auth: {} })
this.services.save()
this.showService(this.services.get().length - 1)
this.newService = ''
}
},
onSaveService({ key, msg }) {
const service = this.loadService(key, this.tile(key).service)
service.name = msg.name
service.auth = msg.auth
this.services.save()
},
onRemoveService({ key }) {
this.services.get().splice(this.tile(key).service, 1)
this.onRemove({ key })
this.services.save()
},
//Errors
onError(event) {
this.addError(event.msg.toString())
@ -36,47 +153,12 @@ var app = new Vue({
removeError(id) {
this.errors.splice(id, 1)
},
//Services
addService() {
if (!this.newService)
return
this.services.push({
type: this.newService,
options: {}, position: {}
})
this.newService = ''
this.showManager = false
this.saveServices()
},
onSave({ key, msg }) {
this.$set(this.services[key].options, msg.key, msg.value)
this.saveServices()
},
onSaveAll({ key, msg }) {
this.$set(this.services, key, {
...this.services[key],
options: msg
})
this.saveServices()
},
onMove({ key, msg }) {
this.$set(this.services[key].position, msg.type, Math.max(1,
(this.services[key].position[msg.type] || 1) + msg.direction
))
this.saveServices()
},
removeService(id) {
this.services.splice(id, 1)
this.saveServices()
},
saveServices() {
localStorage.setItem(servicesStorage, JSON.stringify(this.services))
},
gridPos(id, position = {}) {
//Helpers
gridPos(position = {}) {
return {
'grid-row': `${position.x || 1} / span ${position.h || 2}`,
'grid-column': `${position.y || id*2+1} / span ${position.w || 2}`
'grid-column': `${position.y || 1} / span ${position.w || 2}`
}
},
makeEmiter(key) {

View File

@ -57,33 +57,52 @@ input, select, button
.error
@include tile
#manager
#content
display: flex
flex-direction: column
height: 100vh
#showManager
position: absolute
bottom: 0
#manager
background-color: $tileColor
border-radius: $borderRadius
padding-left: 1em
height: 1.3em
display: flex
justify-content: space-between
#layout-select
display: flex
#services
height: 100vh
flex: 1
overflow: hidden
display: grid
grid-gap: .2em
grid-template-columns: repeat(8, minmax(0, 1fr))
grid-template-rows: repeat(4, minmax(0, 1fr))
justify-items: stretch
& > div
.tile
overflow: auto
display: flex
flex-direction: column
.service-header
.title, .settings
@include tile
.title
font-size: large
text-align: center
font-weight: bold
.settings .position
float: right
width: 1.2em
.service-content
overflow: hidden
& > div
height: 100%
display: flex
flex-direction: column
.service-header
.title, .settings
@include tile
.title
font-size: large
text-align: center
font-weight: bold
.settings .position
float: right
width: 1.2em
.service-content
overflow: hidden
.service-loader
display: inline-block
width: 64px