Remove old scrap

store
sheychen 2019-05-02 12:04:07 +02:00
parent cf1326ca59
commit ddb4890c15
50 changed files with 23 additions and 25855 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
*node_modules
*vscode
*vscode
config.json

View File

@ -1,27 +0,0 @@
{
"extends": [
"eslint:recommended",
"plugin:vue/recommended"
],
"rules": {
"indent": [
"error",
2,
{
"SwitchCase": 1
}
],
"linebreak-style": [
"error",
"windows"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"never"
]
}
}

View 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

@ -1,6 +0,0 @@
// ----------------------------------------------------------------------
// BROWSER COMPONENT ENTRY FILE
// ----------------------------------------------------------------------
import Component from './src/__FILE__';
Vue.component(Component.name, Component);

10699
compiler/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,74 +0,0 @@
{
"name": "vue-browser-component",
"version": "1.0.0",
"description": "A VueJS CLI template for compiling standalone components from .vue files",
"main": "index.js",
"scripts": {
"build": "webpack --env.file",
"serve": "webpack -d --watch --env.file",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/RonnieSan/browser-components.git"
},
"keywords": [
"vue",
"vuejs",
"browser",
"component"
],
"author": "RonnieSan",
"license": "MIT",
"bugs": {
"url": "https://github.com/RonnieSan/browser-components/issues"
},
"homepage": "https://github.com/RonnieSan/browser-components#readme",
"devDependencies": {
"@babel/cli": "^7.2.0",
"@babel/core": "^7.2.2",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.2.0",
"autoprefixer": "^8.3.0",
"babel-loader": "^8.0.5",
"chart.js": "^2.8.0",
"core-js": "^2.6.5",
"css-loader": "^0.28.11",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.2.2",
"file-loader": "^1.1.11",
"friendly-errors-webpack-plugin": "^1.7.0",
"less": "^3.0.2",
"less-loader": "^4.1.0",
"node-sass": "^4.11.0",
"optimize-css-assets-webpack-plugin": "^4.0.0",
"pug": "^2.0.3",
"pug-plain-loader": "^1.0.0",
"sass-loader": "^7.0.1",
"string-replace-webpack-plugin": "^0.1.3",
"url-loader": "^1.0.1",
"vue-chartjs": "^3.4.2",
"vue-loader": "^15.4.2",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.6.0",
"webpack-cli": "^3.2.1"
},
"dependencies": {
"vue": "^2.6.10"
},
"babel": {
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage"
}
]
]
},
"browserslist": [
"last 2 versions",
"ie >= 11"
]
}

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: [
require('autoprefixer')
]
};

View File

@ -1,27 +0,0 @@
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,35 +0,0 @@
<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

@ -1,60 +0,0 @@
<script>
import serviceEmiterVue from './serviceEmiter.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 {
components: {
loadableBlock: loadableBlockVue,
loadableInline: loadableInlineVue,
serviceHeader: serviceHeaderVue,
settingBoolean: settingBooleanVue,
settingInt: settingIntVue,
settingString: settingStringVue
},
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
</script>

View File

@ -1,42 +0,0 @@
<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

@ -1,66 +0,0 @@
<template lang="pug">
span {{ timeSince }}
</template>
<script>
export const timedMixin = {
props: {
now: {
type: Number | Date,
default: Date.now
}
}
}
export default {
mixins: [ timedMixin ],
props: {
date: {
type: [Date, Number, String],
default: Date.now
}
},
computed: {
timeSince() {
var seconds = Math.floor((this.now - new Date(this.date)) / 1000)
var interval = Math.floor(seconds / 31536000)
if (interval > 1) {
return interval + ' years'
}
interval = Math.floor(seconds / 2592000)
if (interval > 1) {
return interval + ' months'
}
interval = Math.floor(seconds / 86400)
if (interval > 1) {
return interval + ' days'
}
interval = Math.floor(seconds / 3600)
if (interval > 1) {
return interval + ' hours'
}
interval = Math.floor(seconds / 60)
if (interval > 1) {
return interval + ' minutes'
}
return Math.floor(seconds) + ' seconds'
}
}
}
export const timerMinin = {
created() {
let self = this
setInterval(() => {
self.now = Date.now()
}, 30 * 1000)
},
data() {
return {
now: Date.now()
}
}
}
</script>

View File

@ -1,19 +0,0 @@
<script>
export default {
props: {
id: {
type: String,
default: undefined
},
title: {
type: String,
default: undefined
}
},
methods: {
sendChange(value) {
this.$emit('change', { key: this.id, value: value })
}
}
}
</script>

View File

@ -1,3 +0,0 @@
p.setting
label(:for="id") {{ title }}:
block input

View File

@ -1,14 +0,0 @@
<template lang="pug">
extends model
block input
input(:id="id" type="checkbox" :checked="value" @change.stop="sendChange($event.target.checked)")
</template>
<script>
import baseSettingVue from './baseSetting.vue'
export default {
extends: baseSettingVue,
props: { value: Boolean }
}
</script>

View File

@ -1,19 +0,0 @@
<template lang="pug">
extends model
block input
input(:id="id" type="number" step="1" :value="value" @keyup.enter="sendChange(parseInt($event.target.value))")
</template>
<script>
import baseSettingVue from './baseSetting.vue'
export default {
extends: baseSettingVue,
props: {
value: {
type: Number,
default: 1
}
}
}
</script>

View File

@ -1,19 +0,0 @@
<template lang="pug">
extends model
block input
input(:id="id" type="text" :value="value" @keyup.enter="sendChange($event.target.value)")
</template>
<script>
import baseSettingVue from './baseSetting.vue'
export default {
extends: baseSettingVue,
props: {
value: {
type: String,
default: undefined
}
}
}
</script>

View File

@ -1,41 +0,0 @@
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
}
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
})
}
}

View File

@ -1,16 +0,0 @@
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

@ -1,22 +0,0 @@
<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

@ -1,21 +0,0 @@
<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

@ -1,37 +0,0 @@
<script>
export default {
props: {
emit: {
type: Function,
default: undefined
}
},
methods:{
emitError(err) {
this.emit('error', err)
},
saveOptions(options) {
this.emit('saveOptions', options)
},
saveOption(key, value) {
this.saveOptionCouple({
key: key, value: value
})
},
saveOptionCouple(couple) {
this.emit('saveOption', couple)
},
saveService(name, auth) {
this.emit('saveService', {
name: name, auth: auth
})
},
catchEmit(req) {
return req.catch(err => {
this.emitError(err)
throw err
})
}
}
}
</script>

View File

@ -1,39 +0,0 @@
<template lang="pug">
.service-header
.title(@click="showSettings = !showSettings")
slot(name="title")
.settings(v-show="showSettings")
input.position(
@keyup.up.ctrl.exact="onMove('x', -1)" @keyup.down.ctrl.exact="onMove('x', 1)"
@keyup.left.ctrl.exact="onMove('y', -1)" @keyup.right.ctrl.exact="onMove('y', 1)"
@keyup.up.alt.exact="onMove('h', -1)" @keyup.down.alt.exact="onMove('h', 1)"
@keyup.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>
<script>
export default {
props: {
emit: {
type: Function,
default: undefined
}
},
data() { return {
showSettings: false
} },
methods: {
onMove(type, direction) {
this.emit('move', { type: type, direction: direction })
},
onRemove() {
this.emit('remove', { })
},
onRemoveService() {
this.emit('removeService', { })
}
}
}
</script>

View File

@ -1,97 +0,0 @@
<template lang="pug">
.client(@scroll.passive="onScroll")
loadable-block.list(:loadable="guilds")
template(#success)
guild(v-for="guild in guilds.get()" :key="guild.id" :guild="guild" :showMedia="options.showMedia")
</template>
<script>
/* global axios */
import { timerMinin } from '../core/fromNow.vue'
import serviceEmiterVue from '../core/serviceEmiter.vue'
import guildVue from './guild.vue'
import Loadable from '../core/loadable/Loadable'
import loadableBlockVue from '../core/loadable/loadableBlock.vue'
export default {
components: {
guild: guildVue,
loadableBlock: loadableBlockVue
},
extends: serviceEmiterVue,
mixins: [ timerMinin ],
props: {
auth: {
type: Object,
default: () => ({})
},
options: { type: Object, default: () => ({}) }
},
data() {
return {
rest: axios.create({
baseURL: 'https://discordapp.com/api/',
headers: { Authorization: this.auth.token },
timeout: this.options.timeout
}),
guilds: new Loadable()
}
},
created() {
this.guilds.load(
this.get('/users/@me/guilds'),
res => res.data)
this.setupStream()
},
methods: {
get(path, 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) {
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)
}*/
}
}
}
</script>

View File

@ -1,27 +0,0 @@
<template lang="pug">
.guild
| {{ guild.name }}
img(v-if="showMedia && guild.icon" :src="`${CDN}/icons/${guild.id}/${guild.icon}.png?size=16`")
</template>
<script>
import { CDN } from './tools.js'
export default {
props: {
guild: {
type: Object,
default: undefined
},
showMedia: {
type: Boolean,
default: true
}
},
computed: {
CDN() {
return CDN
}
}
}
</script>

View File

@ -1,54 +0,0 @@
<template lang="pug">
.discord
service-header(:emit="emit")
template(#title) {{ serviceName }}: {{ account.display() }}
template(#settings)
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(:auth="auth" :options="params" :emit="emit")
template(#error)
form(@submit.prevent="makeAuth")
p
label(for="token") Token:
input#token(v-model="newAuth.token" required)
p
input(type="submit" value="Connect")
</template>
<script>
/* global axios */
import accountServiceVue from '../core/accountService.vue'
import clientVue from './client.vue'
export default { //TODO: Use oauth
name: 'Discord',
components: { client: clientVue },
extends: accountServiceVue,
computed: {
params() {
return { timeout: 5000, reconnect: false, buffer: 20, showMedia: true, ...this.options }
},
isSetup() {
return this.auth && this.auth.token
},
serviceName() {
return 'Discord'
}
},
methods: {
getAccount({ token }) {
return axios.get('https://discordapp.com/api/users/@me', {
headers: { Authorization: token },
timeout: this.params.timeout
})
},
mapAccount(res) {
return res.data.username
}
}
}
</script>

View File

@ -1 +0,0 @@
export const CDN = 'https://cdn.discordapp.com'

View File

@ -1,31 +0,0 @@
<template lang="pug">
a.account(target="_blank" :href="account.url")
.avatar(v-if="showMedia" :style="avatarStyle(account.avatar_static)")
.name(v-html="parseEmojis(account.display_name, account.emojis)")
.acct @{{ account.acct }}
</template>
<script>
import { parseEmojisMixin } from './tools'
export default {
mixins: [ parseEmojisMixin ],
props: {
account: {
type: Object,
default: undefined
},
showMedia: {
type: Boolean,
default: true
}
},
methods: {
avatarStyle(avatar) {
return {
'background-image': `url(${avatar})`
}
}
}
}
</script>

View File

@ -1,146 +0,0 @@
<template lang="pug">
.client(@scroll.passive="onScroll")
.statues
.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="options.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.get()" :key="notification.id" :notification="notification" :now="now"
:showMedia="options.showMedia" @dismiss="onNotificationDismiss" @mark="onStatusMark")
</template>
<script>
/* global axios */
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 ReLoadable from '../core/loadable/ReLoadable'
import loadableBlockVue from '../core/loadable/loadableBlock.vue'
export default {
components: {
status: statusVue,
notification: notificationVue,
loadableBlock: loadableBlockVue
},
extends: serviceEmiterVue,
mixins: [ timerMinin ],
props: {
auth: {
type: Object,
default: () => ({})
},
options: { type: Object, default: () => ({}) }
},
data() {
return {
rest: axios.create({
baseURL: `https://${this.auth.server}/api/v1/`,
headers: { Authorization: 'Bearer ' + this.auth.token },
timeout: this.options.timeout
}),
statues: new ReLoadable(),
notifications: new Loadable()
}
},
computed: {
hasNotifications() {
return this.notifications.isSuccess() && this.notifications.get().length > 0
}
},
created() {
this.statues.load(
this.getTimeline({}),
res => res.data)
this.notifications.load(
this.get('/notifications'),
res => res.data)
this.setupStream()
},
methods: {
get(path, 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))
},
getTimeline(options) {
return this.get('/timelines/home', options)
},
onScroll(event) {
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.get().splice(this.options.buffer)
}
},
removeById(ls, id) {
Lists.removeFirst(ls, e => e.id === id)
},
showStatus(status) {
return (!status.in_reply_to_id || this.options.reply) && (!status.reblog || this.options.reblog)
},
onStatusMark(action) {
this.post(`/statuses/${action.id}/${action.type}`)
.then(action.callback)
},
onNotificationDismiss(id) {
this.post('/notifications/dismiss', { id: id })
.then(() => this.removeById(this.notifications.get(), id))
},
onNotificationsClear() {
this.post('/notifications/clear')
.then(() => Lists.clear(this.notifications.get()))
},
setupStream() {
const ws = new WebSocket(
`wss://${this.auth.server}/api/v1/streaming?access_token=${this.auth.token}&stream=user`
)
ws.onmessage = event => {
event = JSON.parse(event.data)
const payload = JSON.parse(event.payload)
switch (event.event) {
case 'update':
this.statues.get().unshift(payload)
break
case 'notification':
this.notifications.get().unshift(payload)
break
case 'delete':
this.removeById(this.statues.get(), payload.id)
break
}
}
ws.onerror = this.emitError
ws.onclose = () => {
this.emitError(
'Mastodon stream disconnected !' +
(this.options.reconnect ? ' Reconnecting...' : '')
)
if (this.options.reconnect) setTimeout(() => this.setupStream(), this.options.timeout)
}
}
}
}
</script>

View File

@ -1,66 +0,0 @@
<template lang="pug">
.mastodon
service-header(:emit="emit")
template(#title)
| {{ serviceName }}:
loadable-inline(:loadable="account")
template(#success)
span(v-html="parseEmojis(account.data.display_name, account.data.emojis) + '@' + auth.server")
template(#settings)
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(:auth="auth" :options="params" :emit="emit")
template(#error)
form(@submit.prevent="makeAuth")
p
label(for="server") Server:
input#server(v-model="newAuth.server" required)
p
label(for="token") Token:
input#token(v-model="newAuth.token" required)
p
input(type="submit" value="Connect")
</template>
<script>
/* global axios */
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: accountServiceVue,
mixins: [ parseEmojisMixin ],
computed: {
params() {
return { timeout: 5000, reconnect: false, buffer: 20, reblog: true, reply: false,
showMedia: true, ...this.options }
},
isSetup() {
return this.auth && this.auth.server && this.auth.token
},
serviceName() {
return 'Mastodon'
}
},
methods: {
getAccount({ server, token }) {
return axios.get(`https://${server}/api/v1/accounts/verify_credentials`, {
headers: { Authorization: 'Bearer ' + token },
timeout: this.params.timeout
})
},
mapServiceName(res, { server }) {
return `${this.serviceName} ${this.mapAccount(res).acct}@${server}`
}
}
}
</script>

View File

@ -1,50 +0,0 @@
<template lang="pug">
.notification
account(:account="notification.account" :showMedia="showMedia")
span.colored.text-icon.letter(v-if="notification.type == 'mention'")
span.colored.text-icon.letter(v-if="notification.type == 'reblog'")
span.colored.text-icon.letter(v-if="notification.type == 'favourite'")
from-now.date(:date="notification.created_at" :now="now")
.content
template(v-if="notification.type == 'follow'") Vous suit
status.reblog(v-else-if="notification.status" :status="notification.status" :now="now"
:showMedia="showMedia" :withAccount="notification.type != 'mention'" @mark="passMark")
a.date(@click.stop.prevent="makeDismiss" style="margin-top: -1em")
</template>
<script>
import fromNowVue, { timedMixin } from '../core/fromNow.vue'
import accountVue from './account.vue'
import statusVue from './status.vue'
export default {
components: {
fromNow: fromNowVue,
account: accountVue,
status: statusVue
},
mixins: [ timedMixin ],
props: {
notification: {
type: Object,
default: undefined
},
showMedia: {
type: Boolean,
default: true
}
},
methods: {
makeDismiss() {
this.$emit('dismiss', this.notification.id)
},
passMark(action) {
this.$emit('mark', action)
}
}
}
</script>

View File

@ -1,91 +0,0 @@
<template lang="pug">
.status
account(v-if="withAccount" :account="status.account" :showMedia="showMedia")
span.text-icon.letter(v-if="status.reblog")
a.date(target="_blank" :href="status.uri")
from-now(:date="status.created_at" :now="now")
.content(:class="{ avatared: showMedia }")
template(v-if="!status.reblog")
.spoiler(v-if="status.spoiler_text" @click.stop.prevent="status.sensitive = !status.sensitive").
{{ status.spoiler_text || 'Spoiler' }} {{ status.sensitive ? '&rarr;' : '&darr;' }}
div(v-if="!status.spoiler_text || !status.sensitive")
.text(v-html="parseEmojis(status.content, status.emojis)")
a.media(v-for="media in status.media_attachments" :href="media.url" target="_blank")
template(v-if="showMedia")
img(v-if="media.type == 'image' || media.type == 'gifv'" :src="media.preview_url" :alt="media.description" :title="media.description")
.gif(v-if="media.type == 'gifv'") GIF
template(v-else) Hidden media
status.reblog(v-else :status="status.reblog" :now="now" :showMedia="showMedia")
.meta(v-if="!status.reblog")
a.replies(@click.stop.prevent="makeReply(status)")
span.text-icon
| {{ status.replies_count }}
a.reblogs(:class="{ colored: status.reblogged }" @click.stop.prevent="makeReblog(status)")
span.text-icon
| {{ status.reblogs_count }}
a.favourites(:class="{ colored: status.favourited }" @click.stop.prevent="makeFav(status)")
span.text-icon
| {{ status.favourites_count }}
a.fil(v-if="status.in_reply_to_id" @click.stop.prevent="showReply(status.in_reply_to_id)")
| Voir le fil
</template>
<script>
import fromNowVue, { timedMixin } from '../core/fromNow.vue'
import { parseEmojisMixin } from './tools'
import accountVue from './account.vue'
export default {
name: 'Status',
components: {
account: accountVue,
fromNow: fromNowVue
},
mixins: [ timedMixin, parseEmojisMixin ],
props: {
status: {
type: Object,
default: undefined
},
withAccount: {
type: Boolean,
default: true
},
showMedia: {
type: Boolean,
default: true
}
},
methods: {
showReply(statusId) {
console.error(statusId) //TODO:
},
makeReply(status) {
console.error(status.id) //TODO:
},
emitMark(status, action, callback, undo = false) {
this.$emit('mark', {
id: status.id,
type: (undo ? 'un' : '') + action,
callback: callback
})
},
makeReblog(status) {
this.emitMark(status, 'reblog', () => {
status.reblogs_count += (status.reblogged ? -1 : 1)
status.reblogged = !status.reblogged
}, status.reblogged)
},
makeFav(status) {
this.emitMark(status, 'favourite', () => {
status.favourites_count += (status.favourited ? -1 : 1)
status.favourited = !status.favourited
}, status.favourited)
}
}
}
</script>

View File

@ -1,12 +0,0 @@
export const parseEmojisMixin = {
methods: {
parseEmojis(text, emojis) {
for (const emoji of emojis) {
text = text.split(`:${emoji.shortcode}:`).join(
`<img draggable="false" class="icon" alt="${emoji.shortcode}" title="${emoji.shortcode}" src="${emoji.static_url}">`
)
}
return text
}
}
}

View File

@ -1,114 +0,0 @@
<template lang="pug">
.nextcloud-news(v-show="showService")
service-header(:emit="emit")
template(#title)
| {{ serviceName }}
span.note(v-if="hasNews") ({{ news.get().length }})
template(#settings)
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()")
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="newAuth.server" required)
p
label(for="username") Username:
input#username(v-model="newAuth.username" required)
p
label(for="token") Token:
input#token(v-model="newAuth.token" required)
p
input(type="submit" value="Connect")
</template>
<script>
/* global axios */
import connectedServiceVue from '../core/connectedService.vue'
import fromNowVue, { timerMinin } from '../core/fromNow.vue'
import Loadable from '../core/loadable/Loadable'
import Lists from '../core/Lists'
export default {
name: 'NextcloudNews',
components: {
fromNow: fromNowVue
},
extends: connectedServiceVue,
mixins: [ timerMinin ],
data() {
return {
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
},
showService() {
return this.params.showEmpty || this.hasNews || !this.isSetup
}
},
methods: {
loadData() {
this.news.load(
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
})
)
},
removeNews(id) {
Lists.removeFirst(this.news.get(), n => n.id === id)
},
makeRead(id) {
this.catchEmit(this.rest.put(`/items/${id}/read`))
.then(() => this.removeNews(id))
},
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
this.loadData()
if(this.params.update > 0)
setInterval(this.loadData, this.params.update * 1000)
},
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
})
}
}
}
</script>

View File

@ -1,72 +0,0 @@
<script>
import { Bar, mixins } from 'vue-chartjs'
import moment from 'moment'
const { reactiveProp } = mixins
export default {
extends: Bar,
mixins: [ reactiveProp ],
mounted () {
this.renderChart(this.chartData, {
responsive: true, maintainAspectRatio: false,
legend: {
labels: {
fontColor: 'white'
}
},
scales: {
xAxes: [{
type: 'time',
distribution: 'series',
ticks: {
fontColor: 'white',
source: 'data',
autoSkip: true,
maxRotation: 0,
autoSkipPadding: 5
},
time: {
displayFormats: {
hour: 'HH[h]'
}
}
}],
yAxes: [{
id: 'y-axis-temp',
display: true,
position: 'left',
ticks: {
fontColor: 'white'
}
},{
id: 'y-axis-rain',
display: true,
position: 'right',
ticks: {
fontColor: 'white',
beginAtZero: true,
suggestedMax: 1
}
}]
},
tooltips: {
intersect: false,
callbacks: {
title: function(tooltipItem, myData) {
var item = myData.datasets[tooltipItem[0].datasetIndex].data[tooltipItem[0].index]
return moment(item.x || item.t).format('HH[h]')
},
label: function(tooltipItem, myData) {
var label = myData.datasets[tooltipItem.datasetIndex].label || ''
if (label) {
label += ': '
}
label += tooltipItem.value
return label
}
}
}
})
}
}
</script>

View File

@ -1,166 +0,0 @@
<template lang="pug">
.openweathermap
service-header(:emit="emit")
template(#title) {{ serviceName }}
template(#settings)
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")
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="newAuth.token" required)
p
input(type="submit" value="Connect")
</template>
<script>
/* global axios */
import connectedServiceVue from '../core/connectedService.vue'
import Lists from '../core/Lists.js'
import Loadable from '../core/loadable/Loadable'
import chartVue from './chart.vue'
import weatherVue from './weather.vue'
export default {
name: 'Openweathermap',
components: {
weather: weatherVue,
chart: chartVue
},
extends: connectedServiceVue,
data() {
return {
rest: undefined, //NOTE: set in this.init()
weathers: new Loadable(),
forecast: new Loadable(),
selectedId: 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',
label: 'Temperature',
yAxisID: 'y-axis-temp',
borderColor: 'white',
borderWidth: 1,
fill: false,
data: this.forecast.get().map(function (line) { return {
x: line.dt * 1000, y: line.main.temp
} })
},{
type: 'bar',
label: 'Percipitation',
yAxisID: 'y-axis-rain',
borderColor: '#DDDDDD',
backgroundColor: '#DDDDDD33',
borderWidth: 1,
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
}
},
created() {
this.$watch('options.cities', this.init)
},
methods: {
makeSelect(id) {
this.selectedId = id
this.loadForecast()
},
updateData() {
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.selected) {
this.forecast.load(
this.catchEmit(this.rest.get('forecast', { params: {
id: this.selected.id, cnt: this.params.forecastLimit
}})),
res => res.data.list
)
} else this.forecast.fail('Any selection')
},
formatDate(dt) {
const date = new Date(dt * 1000)
return `${date.toLocaleDateString()} ${date.getHours()}h`
},
addCity(id) {
this.params.cities.push({ id: id })
this.saveOption('cities', this.params.cities)
},
removeCity(key) {
Lists.removeAt(this.params.cities, key)
this.saveOption('cities', this.params.cities)
},
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.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([])
},
checkAuth({ token }) {
return axios.get('https://api.openweathermap.org/data/2.5/weather', {
params: { q: 'London', appid: token },
timeout: this.params.timeout
})
}
}
}
</script>

View File

@ -1,25 +0,0 @@
<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: {
type: Object,
default: undefined
},
selected: Boolean
}
}
</script>

View File

@ -1,127 +0,0 @@
// ----------------------------------------------------------------------
// WEBPACK CONFIGURATION
// ----------------------------------------------------------------------
// INSTRUCTIONS
// webpack --env.file="./path/to/file" --relative to the src folder
// Import dependencies
const path = require('path');
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin');
const StringReplacePlugin = require('string-replace-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
function resolve(dir) {
return path.resolve(__dirname, dir);
}
module.exports = (env) => {
// Get the basename from the filepath
const filename = path.basename(env.file, '.vue');
const filepath = path.dirname(env.file);
return {
mode : 'production',
entry : {
[filename] : './entry.js'
},
performance: {
maxEntrypointSize: 512000,
maxAssetSize: 512000
},
output : {
filename : '[name].js',
path : path.resolve(__dirname, 'dist', filepath)
},
resolve : {
extensions : ['.vue', '.js'],
alias : {
'vue$' : resolve('node_modules/vue/dist/vue.min.js'),
'@' : resolve('src')
}
},
externals : {
vue : 'Vue',
lodash : 'lodash'
},
module : {
rules : [
{
test : /entry\.js$/,
loader : StringReplacePlugin.replace({
replacements: [
{
pattern: /__FILE__/ig,
replacement: function (match, p1, offset, string) {
return env.file;
}
}
]})
},
{
test : /\.vue$/,
loader : 'vue-loader'
},
{
test : /\.js$/,
loader : 'babel-loader',
include : [
resolve('src')
],
exclude: file => (
/node_modules/.test(file) &&
!/\.vue\.js/.test(file)
)
},
{
test : /\.css$/,
use : [
'vue-style-loader',
'css-loader'
]
},
{
test : /\.less$/,
use : [
'vue-style-loader',
'css-loader',
'less-loader'
]
},
{
test : /\.scss$/,
use : [
'vue-style-loader',
'css-loader',
'sass-loader'
]
},
{
test : /\.sass$/,
use : [
'vue-style-loader',
'css-loader',
{
loader : 'sass-loader',
options : {
indentedSyntax : true
}
}
]
},
{
test: /\.pug$/,
loader: 'pug-plain-loader'
}
]
},
plugins : [
new VueLoaderPlugin(),
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
})
]
};
};

View File

@ -1,57 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Mixit</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="main.css">
<script src="lib/axios.js"></script>
<script src="lib/vue.full.js"></script>
<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>
<div id="app">
<div id="errors" v-show="errors">
<p class="error" v-for="(error, key) in errors" @click="removeError(key)">{{ error }}</p>
</div>
<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>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

426
main.css
View File

@ -1,426 +0,0 @@
* {
margin: 0;
padding: 0;
font-family: Verdana, Geneva, sans-serif;
scrollbar-width: thin;
}
body {
background-color: #333;
color: #eee;
}
a {
text-decoration: none;
color: #aaa;
}
input, select, button {
background-color: #333;
color: #eee;
border: 1px solid #999;
}
.icon {
width: 1em;
height: 1em;
vertical-align: middle;
}
.text-icon {
font-weight: bold;
font-size: 1.2em;
}
.note {
font-size: .7em;
vertical-align: text-top;
}
.letter {
margin: 0 .5em;
}
.colored {
color: orange;
}
#errors {
position: absolute;
}
#errors .error {
margin: 0.3em;
background-color: #222;
border-radius: 0.3em;
padding: 0.3em;
}
#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 {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
overflow: hidden;
display: -ms-grid;
display: grid;
grid-gap: .2em;
-ms-grid-columns: (minmax(0, 1fr))[8];
grid-template-columns: repeat(8, minmax(0, 1fr));
-ms-grid-rows: (minmax(0, 1fr))[4];
grid-template-rows: repeat(4, minmax(0, 1fr));
justify-items: stretch;
}
#services .tile {
overflow: auto;
}
#services .tile > div {
height: 100%;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
}
#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 .tile > div .service-header .title {
font-size: large;
text-align: center;
font-weight: bold;
}
#services .tile > div .service-header .settings .position {
float: right;
width: 1.2em;
}
#services .tile > div .service-content {
overflow: hidden;
}
#services .service-loader {
display: inline-block;
width: 64px;
height: 64px;
}
#services .service-loader:after {
content: " ";
display: block;
width: 46px;
height: 46px;
margin: 1px;
border-radius: 50%;
border: 5px solid #aaa;
border-color: #aaa transparent #aaa transparent;
-webkit-animation: service-loader 1.2s linear infinite;
animation: service-loader 1.2s linear infinite;
}
@-webkit-keyframes service-loader {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes service-loader {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.mastodon .client {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
height: 100%;
overflow-y: auto;
}
.mastodon .client .header {
margin: 0.3em;
background-color: #222;
border-radius: 0.3em;
padding: 0.3em;
}
.mastodon .client .list > div {
margin: 0.3em;
background-color: #222;
border-radius: 0.3em;
padding: 0.3em;
}
.mastodon .client .statues {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
}
.mastodon .client .notifications {
max-width: 33%;
}
.mastodon .client .account .name {
margin: 0 0.3em;
color: #eee;
}
.mastodon .client .account .avatar {
float: left;
border-radius: 0.3em;
width: 3em;
height: 3em;
background-size: 3em 3em;
}
.mastodon .client .account div {
display: inline-block;
}
.mastodon .client .status, .mastodon .client .notification {
min-height: 3em;
}
.mastodon .client .status .content, .mastodon .client .notification .content {
margin: .5em .5em .5em 1em;
}
.mastodon .client .status .content.avatared, .mastodon .client .notification .content.avatared {
margin-left: 3.5em;
}
.mastodon .client .status .content .reblog, .mastodon .client .notification .content .reblog {
font-size: .8em;
}
.mastodon .client .status .content .spoiler, .mastodon .client .notification .content .spoiler {
margin-bottom: .5em;
}
.mastodon .client .status .content .media, .mastodon .client .notification .content .media {
margin: .5em;
position: relative;
display: inline-block;
}
.mastodon .client .status .content .media > *, .mastodon .client .notification .content .media > * {
max-height: 10em;
max-width: 100%;
}
.mastodon .client .status .content .media .gif, .mastodon .client .notification .content .media .gif {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
height: 100%;
width: 100%;
background-color: #00000044;
color: white;
padding: .5em;
}
.mastodon .client .status .meta, .mastodon .client .notification .meta {
margin-left: 4em;
font-size: .8em;
}
.mastodon .client .status .meta a, .mastodon .client .notification .meta a {
margin: 0 .5em;
}
.mastodon .client .date, .mastodon .client .dismiss {
float: right;
}
.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;
flex-direction: column;
}
.openweathermap .list {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.openweathermap .weather, .openweathermap .forecast {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
margin: 0.3em;
background-color: #222;
border-radius: 0.3em;
padding: 0.3em;
}
.openweathermap .forecast {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
overflow: hidden;
height: 100%;
}
.openweathermap .forecast .chart {
position: relative;
height: 100%;
}
.openweathermap .weather {
min-width: 17em;
border: 1px solid #222;
display: -ms-grid;
display: grid;
-ms-grid-columns: auto auto;
grid-template-columns: auto auto;
-ms-grid-rows: 1.2em auto;
grid-template-rows: 1.2em auto;
grid-template-areas: "header main" "data remove";
}
.openweathermap .weather.selected {
border-color: #999;
}
.openweathermap .weather .header {
-ms-grid-row: 1;
-ms-grid-column: 1;
grid-area: header;
font-size: 1.2em;
}
.openweathermap .weather .data {
-ms-grid-row: 2;
-ms-grid-column: 1;
grid-area: data;
margin-top: .5em;
}
.openweathermap .weather .main {
-ms-grid-row: 1;
-ms-grid-column: 2;
grid-area: main;
-ms-grid-column-align: right;
justify-self: right;
}
.openweathermap .weather .main p {
margin: 0.3em;
display: inline;
vertical-align: top;
}
.openweathermap .weather .remove {
-ms-grid-row: 2;
-ms-grid-column: 2;
grid-area: remove;
-ms-grid-column-align: right;
justify-self: right;
-ms-flex-item-align: end;
-ms-grid-row-align: end;
align-self: end;
font-size: .8em;
}
.openweathermap .ic {
overflow: hidden;
height: 30px;
display: inline-block;
}
.openweathermap .ic img {
margin-top: -10px;
}
.nextcloud-news .unreaded {
overflow-y: auto;
}
.nextcloud-news .news {
margin: 0.3em;
background-color: #222;
border-radius: 0.3em;
padding: 0.3em;
}
.nextcloud-news .news .date {
float: right;
}
.nextcloud-news .news .read {
margin-right: .5em;
}
.nextcloud-news .news .content {
padding: 0.3em;
}
/*# sourceMappingURL=main.css.map */

File diff suppressed because one or more lines are too long

171
main.js
View File

@ -1,171 +0,0 @@
/* 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: '',
errors: [],
bus: new Vue()
},
mounted() {
this.layouts.load()
this.services.load()
this.bus.$on('error', this.onError)
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())
},
addError(err) {
this.errors.push(err)
},
removeError(id) {
this.errors.splice(id, 1)
},
//Helpers
gridPos(position = {}) {
return {
'grid-row': `${position.x || 1} / span ${position.h || 2}`,
'grid-column': `${position.y || 1} / span ${position.w || 2}`
}
},
makeEmiter(key) {
const self = this
return function(name, msg) {
self.bus.$emit(name, { msg: msg, key: key })
}
}
}
})

254
main.sass
View File

@ -1,254 +0,0 @@
$avatarSize: 3em
$borderRadius: .3em
$backColor: #333
$tileColor: #222
$darkColor: #111
$halfColor: #999
$noneColor: #aaa
$foreColor: #eee
@mixin tile
margin: $borderRadius
background-color: $tileColor
border-radius: $borderRadius
padding: $borderRadius
*
margin: 0
padding: 0
font-family: Verdana, Geneva, sans-serif
scrollbar-width: thin
body
background-color: $backColor
color: $foreColor
a
text-decoration: none
color: $noneColor
input, select, button
background-color: $backColor
color: $foreColor
border: 1px solid $halfColor
.icon
width: 1em
height: 1em
vertical-align: middle
.text-icon
font-weight: bold
font-size: 1.2em
.note
font-size: .7em
vertical-align: text-top
.letter
margin: 0 .5em
.colored
color: orange
#errors
position: absolute
.error
@include tile
#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
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
.tile
overflow: auto
& > 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
height: 64px
&:after
content: " "
display: block
width: 46px
height: 46px
margin: 1px
border-radius: 50%
border: 5px solid $noneColor
border-color: $noneColor transparent $noneColor transparent
animation: service-loader 1.2s linear infinite
@keyframes service-loader
0%
transform: rotate(0deg)
100%
transform: rotate(360deg)
.mastodon
.client
display: flex
height: 100%
overflow-y: auto
.header
@include tile
.list
& > div
@include tile
.statues
flex: 1
.notifications
max-width: 33%
.account
.name
margin: 0 $borderRadius
color: $foreColor
.avatar
float: left
border-radius: $borderRadius
width: $avatarSize
height: $avatarSize
background-size: $avatarSize $avatarSize
div
display: inline-block
.status, .notification
min-height: $avatarSize
.content
margin: .5em .5em .5em 1em
&.avatared
margin-left: .5em + $avatarSize
.reblog
font-size: .8em
.spoiler
margin-bottom: .5em
.media
margin: .5em
position: relative
display: inline-block
& > *
max-height: 10em
max-width: 100%
.gif
position: absolute
top: 0
bottom: 0
left: 0
right: 0
height: 100%
width: 100%
background-color: #00000044
color: white
padding: .5em
.meta
margin-left: 1em + $avatarSize
font-size: .8em
a
margin: 0 .5em
.date, .dismiss
float: right
.openweathermap
.loadable-block
overflow: hidden
display: flex
flex: 1
flex-direction: column
.list
display: flex
flex-wrap: wrap
.weather, .forecast
flex: 1
@include tile
.forecast
flex: 1
overflow: hidden
height: 100%
.chart
position: relative
height: 100%
.weather
min-width: 17em
border: 1px solid $tileColor
display: grid
grid-template-columns: auto auto
grid-template-rows: 1.2em auto
grid-template-areas: "header main" "data remove"
&.selected
border-color: $halfColor
.header
grid-area: header
font-size: 1.2em
.data
grid-area: data
margin-top: .5em
.main
grid-area: main
justify-self: right
p
margin: $borderRadius
display: inline
vertical-align: top
.remove
grid-area: remove
justify-self: right
align-self: end
font-size: .8em
.ic
overflow: hidden
height: 30px
display: inline-block
img
margin-top: -10px
.nextcloud-news
.unreaded
overflow-y: auto
.news
@include tile
.date
float: right
.read
margin-right: .5em
.content
padding: $borderRadius

28
package-lock.json generated
View File

@ -4680,12 +4680,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -4700,17 +4702,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -4827,7 +4832,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -4839,6 +4845,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -4853,6 +4860,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -4860,12 +4868,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -4884,6 +4894,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -4964,7 +4975,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -4976,6 +4988,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -5097,6 +5110,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",