Initial commit
This commit is contained in:
commit
1ac0360671
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,6 @@
|
|||
// ----------------------------------------------------------------------
|
||||
// BROWSER COMPONENT ENTRY FILE
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
import Component from './src/__FILE__';
|
||||
Vue.component(Component.name, Component);
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"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",
|
||||
"css-loader": "^0.28.11",
|
||||
"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",
|
||||
"sass-loader": "^7.0.1",
|
||||
"string-replace-webpack-plugin": "^0.1.3",
|
||||
"url-loader": "^1.0.1",
|
||||
"vue-loader": "^15.4.2",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"vue-template-compiler": "^2.5.22",
|
||||
"webpack": "^4.6.0",
|
||||
"webpack-cli": "^3.2.1",
|
||||
"pug": "^2.0.3",
|
||||
"pug-plain-loader": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^2.5.22"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"ie >= 11"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')
|
||||
]
|
||||
};
|
|
@ -0,0 +1,61 @@
|
|||
<template lang="pug">
|
||||
span {{ timeSince }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export const timedMixin = {
|
||||
props: {
|
||||
now: {
|
||||
type: Number | Date,
|
||||
default: Date.now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
mixins: [ timedMixin ],
|
||||
props: [ 'date' ],
|
||||
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>
|
|
@ -0,0 +1,15 @@
|
|||
export const emitErrorMixin = {
|
||||
methods: {
|
||||
emitError(err) {
|
||||
this.$emit("error", err.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const saveOptionsMixin = {
|
||||
methods: {
|
||||
saveOptions(options) {
|
||||
this.$emit("save", options)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<template lang="pug">
|
||||
div(@click="changeName()").
|
||||
Hello, compiler.
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name : 'example',
|
||||
data() {
|
||||
return {
|
||||
name : 'world'
|
||||
};
|
||||
},
|
||||
methods : {
|
||||
changeName() {
|
||||
this.name = 'foobar';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
div
|
||||
font-weight: bold
|
||||
</style>
|
|
@ -0,0 +1,22 @@
|
|||
<template lang="pug">
|
||||
a.account(target="_blank" :href="account.url")
|
||||
.avatar(: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 {
|
||||
props: ["account"],
|
||||
mixins: [ parseEmojisMixin ],
|
||||
methods: {
|
||||
avatarStyle(avatar) {
|
||||
return {
|
||||
"background-image": `url(${avatar})`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,140 @@
|
|||
<template lang="pug">
|
||||
.client
|
||||
.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" @mark="onStatusMark")
|
||||
template(v-else) Loading...
|
||||
.notifications(v-if="notifications.length > 0")
|
||||
.header
|
||||
| Notifications
|
||||
span.date(@click.stop.prevent="onNotificationsClear") ❌
|
||||
.list
|
||||
notification(v-for="notification in notifications" :key="notification.id" :notification="notification" :now="now" @dismiss="onNotificationDismiss")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { timerMinin } from '../core/fromNow.vue'
|
||||
import { emitErrorMixin, saveOptionsMixin } from '../core/tools'
|
||||
import statusVue from './status.vue'
|
||||
import notificationVue from './notification.vue'
|
||||
|
||||
export default {
|
||||
mixins: [ timerMinin, emitErrorMixin ],
|
||||
components: {
|
||||
status: statusVue,
|
||||
notification: notificationVue
|
||||
},
|
||||
props: {
|
||||
server: String,
|
||||
token: String,
|
||||
timeout: Number,
|
||||
reconnect: Boolean,
|
||||
buffer: Number,
|
||||
reblog: Boolean,
|
||||
reply: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rest: axios.create({
|
||||
baseURL: `https://${this.server}/api/v1/`,
|
||||
timeout: this.timeout,
|
||||
headers: {
|
||||
Authorization: "Bearer " + this.token
|
||||
}
|
||||
}),
|
||||
statues: [],
|
||||
notifications: [],
|
||||
now: Date.now()
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
addStatus(status) {
|
||||
this.statues.unshift(status)
|
||||
this.statues.splice(this.buffer)
|
||||
},
|
||||
addNotification(notif) {
|
||||
this.notifications.push(notif)
|
||||
this.notifications.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
|
||||
}
|
||||
}
|
||||
},
|
||||
showStatus(status) {
|
||||
return (!status.in_reply_to_id || this.reply) && (!status.reblog || this.reblog)
|
||||
},
|
||||
onStatusMark(action) {
|
||||
this.rest.post(`/statuses/${action.id}/${action.type}`)
|
||||
.then(action.callback)
|
||||
.catch(this.emitError)
|
||||
},
|
||||
onNotificationDismiss(id) {
|
||||
this.rest.post('/notifications/dismiss', { id: id })
|
||||
.then(() => this.removeNotification(id))
|
||||
.catch(this.emitError)
|
||||
},
|
||||
onNotificationsClear() {
|
||||
this.rest.post('/notifications/clear')
|
||||
.then(() => this.notifications.splice(0, this.notifications.length))
|
||||
.catch(this.emitError)
|
||||
},
|
||||
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.addStatus(payload)
|
||||
break
|
||||
|
||||
case "notification":
|
||||
this.addNotification(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.rest
|
||||
.get("/notifications", { params: { limit: this.buffer } })
|
||||
.then(res => this.notifications.push.apply(this.notifications, res.data))
|
||||
.catch(this.emitError)
|
||||
|
||||
this.setupStream()
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,106 @@
|
|||
<template lang="pug">
|
||||
.mastodon
|
||||
.header(@click="showSettings = !showSettings")
|
||||
| Mastodon:
|
||||
span(v-html="parseEmojis(account.display_name, account.emojis)")
|
||||
| @{{ server }}
|
||||
.settings(v-show="showSettings")
|
||||
p
|
||||
label(for="reconnect") Reconnect:
|
||||
input#reconnect(type="checkbox" :checked="reconnect" @change="setOption('reconnect', $event.target.checked)")
|
||||
p
|
||||
label(for="reblog") Show reblogs:
|
||||
input#reblog(type="checkbox" :checked="reblog" @change="setOption('reblog', $event.target.checked)")
|
||||
p
|
||||
label(for="reply") Show replies:
|
||||
input#reply(type="checkbox" :checked="reply" @change="setOption('reply', $event.target.checked)")
|
||||
p
|
||||
label(for="buffer") Buffer:
|
||||
input#buffer(type="number" :value="buffer" @keyup.enter="setOption('buffer', parseInt($event.target.value))")
|
||||
client(v-if="server && token" v-bind="$props")
|
||||
.auth(v-else)
|
||||
form(@submit.prevent="setServer")
|
||||
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 { emitErrorMixin, saveOptionsMixin } from '../core/tools'
|
||||
import { parseEmojisMixin } from './tools'
|
||||
import clientVue from './client.vue'
|
||||
|
||||
export default { //TODO: Use oauth
|
||||
name: 'mastodon',
|
||||
mixins: [ emitErrorMixin, saveOptionsMixin, parseEmojisMixin ],
|
||||
components: {
|
||||
client: clientVue
|
||||
},
|
||||
props: {
|
||||
server: String,
|
||||
token: String,
|
||||
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
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newServer: this.server,
|
||||
newToken: this.token,
|
||||
showSettings: false,
|
||||
account: { display_name: 'Loading...', emojis: [] }
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
setServer() {
|
||||
axios.get(`https://${this.newServer}/api/v1/accounts/verify_credentials`, {
|
||||
headers: { Authorization: "Bearer " + this.newToken },
|
||||
timeout: this.timeout
|
||||
}).then(() => this.saveOptions({...this.$props,
|
||||
server: this.newServer, token: this.newToken}))
|
||||
.catch(this.emitError)
|
||||
},
|
||||
setOption(name, value) {
|
||||
const options = {...this.$props}
|
||||
options[name] = value
|
||||
this.saveOptions(options)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if(this.server && this.token) {
|
||||
axios.get(`https://${this.server}/api/v1/accounts/verify_credentials`, {
|
||||
headers: { Authorization: "Bearer " + this.token },
|
||||
timeout: this.timeout
|
||||
}).then(res => this.account = res.data)
|
||||
.catch(err => {
|
||||
this.emitError(err)
|
||||
this.account.display_name = 'Failed'
|
||||
})
|
||||
} else{
|
||||
this.account.display_name = 'First connection'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,38 @@
|
|||
<template lang="pug">
|
||||
.notification
|
||||
account(:account="notification.account")
|
||||
|
||||
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"
|
||||
:withAccount="notification.type != 'mention'" @mark.stop.prevent="")
|
||||
|
||||
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 {
|
||||
props: ["notification"],
|
||||
mixins: [ timedMixin ],
|
||||
components: {
|
||||
fromNow: fromNowVue,
|
||||
account: accountVue,
|
||||
status: statusVue
|
||||
},
|
||||
methods: {
|
||||
makeDismiss() {
|
||||
this.$emit('dismiss', this.notification.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,83 @@
|
|||
<template lang="pug">
|
||||
div
|
||||
.status
|
||||
account(v-if="withAccount" :account="status.account")
|
||||
|
||||
span.text-icon.letter(v-if="status.reblog") ⟳
|
||||
|
||||
a.date(target="_blank" :href="status.uri")
|
||||
from-now(:date="status.created_at" :now="now")
|
||||
|
||||
.content
|
||||
template(v-if="!status.reblog")
|
||||
.spoiler(v-if="status.spoiler_text" @click.stop.prevent="status.sensitive = !status.sensitive").
|
||||
{{ status.spoiler_text || 'Spoiler' }} {{ status.sensitive ? '→' : '↓' }}
|
||||
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")
|
||||
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
|
||||
status.reblog(v-else :status="status.reblog" :now="now")
|
||||
|
||||
.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",
|
||||
mixins: [ timedMixin, parseEmojisMixin ],
|
||||
components: {
|
||||
account: accountVue,
|
||||
fromNow: fromNowVue
|
||||
},
|
||||
props: {
|
||||
status: Object,
|
||||
withAccount: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showReply(statusId) {
|
||||
console.error(statusId) //TODO:
|
||||
},
|
||||
makeReply(status) {
|
||||
console.error(status.id) //TODO:
|
||||
},
|
||||
emitMark(status, action, callback) {
|
||||
this.$emit('mark', {
|
||||
id: status.id,
|
||||
type: (status.reblogged ? 'un' : '') + action,
|
||||
callback: callback
|
||||
})
|
||||
},
|
||||
makeReblog(status) {
|
||||
this.emitMark(status, 'reblog', () => {
|
||||
status.reblogs_count += (status.reblogged ? -1 : 1)
|
||||
status.reblogged = !status.reblogged
|
||||
})
|
||||
},
|
||||
makeFav(status) {
|
||||
this.emitMark(status, 'favourite', () => {
|
||||
status.favourites_count += (status.favourited ? -1 : 1)
|
||||
status.favourited = !status.favourited
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,12 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
<template lang="pug">
|
||||
.nextcloud-news(v-show="unreaded.length > 0 || !server || !token || !username")
|
||||
.header(@click="showSettings = !showSettings") Nextcloud News
|
||||
.settings(v-show="showSettings")
|
||||
p
|
||||
label(for="update") Update interval:
|
||||
input#update(type="number" :value="update" @keyup.enter="setOption('update', parseInt($event.target.value))")
|
||||
p
|
||||
label(for="buffer") Buffer:
|
||||
input#buffer(type="number" :value="buffer" @keyup.enter="setOption('buffer', parseInt($event.target.value))")
|
||||
.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")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { emitErrorMixin, saveOptionsMixin } from '../core/tools'
|
||||
import fromNowVue, { timerMinin } from '../core/fromNow.vue';
|
||||
|
||||
export default {
|
||||
name: 'nextcloud-news',
|
||||
mixins: [ emitErrorMixin, timerMinin, saveOptionsMixin ],
|
||||
components: {
|
||||
fromNow: fromNowVue
|
||||
},
|
||||
props: {
|
||||
server: String,
|
||||
username: String,
|
||||
token: String,
|
||||
timeout: {
|
||||
default: 5000,
|
||||
type: Number
|
||||
},
|
||||
buffer : {
|
||||
default: -1,
|
||||
type: Number
|
||||
},
|
||||
update: {
|
||||
default: 5 * 60, //5min
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
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)
|
||||
}
|
||||
}),
|
||||
unreaded: [],
|
||||
now: Date.now(),
|
||||
showSettings: false,
|
||||
newServer: this.server,
|
||||
newUsername: this.username,
|
||||
newToken: this.token,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
this.rest.get("/items", { params: { batchSize: this.buffer, type: 3, getRead: false } })
|
||||
.then(res => this.unreaded = res.data.items.map(n => {
|
||||
n.open = false
|
||||
return n
|
||||
}))
|
||||
.catch(this.emitError)
|
||||
},
|
||||
removeNews(id) {
|
||||
for (var i = this.unreaded.length - 1; i >= 0; i--) {
|
||||
if (this.unreaded[i].id === id) {
|
||||
this.unreaded.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
makeRead(id) {
|
||||
this.rest.put(`/items/${id}/read`)
|
||||
.then(() => this.removeNews(id))
|
||||
.catch(this.emitError)
|
||||
},
|
||||
setServer() {
|
||||
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 }))
|
||||
.catch(this.emitError)
|
||||
},
|
||||
setOption(name, value) {
|
||||
const options = {...this.$props}
|
||||
options[name] = value
|
||||
this.saveOptions(options)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if(this.server) {
|
||||
this.loadData()
|
||||
|
||||
if(this.update > 0)
|
||||
setInterval(this.loadData, this.update * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,138 @@
|
|||
<template lang="pug">
|
||||
.openweathermap
|
||||
.header(@click="showSettings = !showSettings") OpenWeatherMap
|
||||
.settings(v-show="showSettings")
|
||||
p
|
||||
label(for="token") Token:
|
||||
input#token(:value="token" @keyup.enter="setOption('token', $event.target.value)")
|
||||
p
|
||||
label(for="update") Update interval:
|
||||
input#update(type="number" :value="update" @keyup.enter="setOption('update', parseInt($event.target.value))")
|
||||
p
|
||||
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")
|
||||
.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
|
||||
template(v-if="forecast")
|
||||
.list
|
||||
.line(v-for="line in forecast")
|
||||
| {{ formatDate(line.dt) }}
|
||||
.data
|
||||
| {{ line.main.temp }}°C ─ {{ line.main.humidity }}%
|
||||
.main(v-for="main in line.weather")
|
||||
.ic
|
||||
img(:src="`https://openweathermap.org/img/w/${main.icon}.png`" :alt="main.main")
|
||||
p {{ main.description }}
|
||||
template(v-else) Loading...
|
||||
template(v-else) Loading...
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { emitErrorMixin, saveOptionsMixin } from '../core/tools'
|
||||
|
||||
export default {
|
||||
name: 'openweathermap',
|
||||
components: {},
|
||||
mixins: [ emitErrorMixin, saveOptionsMixin ],
|
||||
props: {
|
||||
token: String,
|
||||
cities: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
timeout: {
|
||||
default: 5000,
|
||||
type: Number
|
||||
},
|
||||
update: {
|
||||
default: 10 * 60, //10min
|
||||
type: Number
|
||||
},
|
||||
lang: {
|
||||
default: 'fr',
|
||||
type: String
|
||||
}
|
||||
},
|
||||
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
|
||||
}),
|
||||
weathers: [],
|
||||
forecast: null,
|
||||
selected: 0,
|
||||
showSettings: false,
|
||||
showAdd: this.cities.length == 0
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
makeSelect(id) {
|
||||
this.selected = id
|
||||
this.forecast = null
|
||||
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))
|
||||
}
|
||||
this.loadForecast()
|
||||
},
|
||||
getWeather(params) {
|
||||
return this.rest.get('weather', { params: params })
|
||||
.catch(this.emitError)
|
||||
},
|
||||
loadForecast() {
|
||||
if(this.weathers[this.selected]) {
|
||||
this.rest.get('forecast', { params: {
|
||||
id: this.weathers[this.selected].id
|
||||
}})
|
||||
.then(res => this.forecast = res.data.list)
|
||||
.catch(this.emitError)
|
||||
}
|
||||
},
|
||||
formatDate(dt) {
|
||||
const date = new Date(dt * 1000)
|
||||
return `${date.toLocaleDateString()} ${date.getHours()}h`
|
||||
},
|
||||
addCity(id) {
|
||||
const options = {...this.$props}
|
||||
options.cities.push({id: id})
|
||||
this.saveOptions(options)
|
||||
},
|
||||
setOption(name, value) {
|
||||
const options = {...this.$props}
|
||||
options[name] = value
|
||||
this.saveOptions(options)
|
||||
}
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,123 @@
|
|||
// ----------------------------------------------------------------------
|
||||
// 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'
|
||||
},
|
||||
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
|
||||
}
|
||||
})
|
||||
]
|
||||
};
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
<!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>
|
||||
</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="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" :key="key" v-bind="service.options" @error="addError" @save="setService(key, $event)"></component>
|
||||
</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
|
@ -0,0 +1,317 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #333;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.text-icon {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
#manager {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#services {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#services > div {
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mastodon .header, .mastodon .settings, .mastodon .client .list > div {
|
||||
margin: 0.3em;
|
||||
background-color: #222;
|
||||
border-radius: 0.3em;
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
.mastodon .header {
|
||||
font-size: large;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mastodon .client {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mastodon .client .list {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
min-height: -webkit-min-content;
|
||||
min-height: -moz-min-content;
|
||||
min-height: min-content;
|
||||
}
|
||||
|
||||
.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: 0.5em 0.5em 0.5em 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 {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
.openweathermap > .header, .openweathermap .settings {
|
||||
margin: 0.3em;
|
||||
background-color: #222;
|
||||
border-radius: 0.3em;
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
.openweathermap > .header {
|
||||
font-size: large;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.openweathermap .ic {
|
||||
overflow: hidden;
|
||||
height: 30px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.openweathermap .ic img {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.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;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
max-height: 100%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.openweathermap .forecast .line {
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
min-width: 15em;
|
||||
margin: 0.3em;
|
||||
background-color: #222;
|
||||
border-radius: 0.3em;
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
.openweathermap .forecast .line .data {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.openweathermap .forecast .line .main p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.openweathermap .weather {
|
||||
min-width: 17em;
|
||||
border: 1px solid #222;
|
||||
}
|
||||
|
||||
.openweathermap .weather.selected {
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.openweathermap .weather .header {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.openweathermap .weather .data {
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
.openweathermap .weather .main {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.openweathermap .weather .main p {
|
||||
margin: 0.3em;
|
||||
display: inline;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.nextcloud-news > .header, .nextcloud-news .settings {
|
||||
margin: 0.3em;
|
||||
background-color: #222;
|
||||
border-radius: 0.3em;
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
.nextcloud-news > .header {
|
||||
font-size: large;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.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 */
|
|
@ -0,0 +1,57 @@
|
|||
//TODO: discord, weather graph
|
||||
|
||||
const servicesStorage = 'services'
|
||||
var app = new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
showManager: false,
|
||||
newService: '',
|
||||
services: [],
|
||||
errors: []
|
||||
},
|
||||
mounted() {
|
||||
if (localStorage.getItem(servicesStorage)) {
|
||||
try {
|
||||
this.services = JSON.parse(localStorage.getItem(servicesStorage))
|
||||
} catch (e) {
|
||||
localStorage.removeItem(servicesStorage)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addError(err) {
|
||||
this.errors.push(err)
|
||||
},
|
||||
removeError(id) {
|
||||
this.errors.splice(id, 1)
|
||||
},
|
||||
addService() {
|
||||
// ensure they actually typed something
|
||||
if (!this.newService) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.services.push({
|
||||
type: this.newService,
|
||||
options: {}
|
||||
})
|
||||
this.newService = ''
|
||||
this.showManager = false
|
||||
this.saveServices()
|
||||
},
|
||||
setService(id, options) {
|
||||
this.$set(this.services, id, {
|
||||
type: this.services[id].type,
|
||||
options: options
|
||||
})
|
||||
this.saveServices()
|
||||
},
|
||||
removeService(id) {
|
||||
this.services.splice(id, 1)
|
||||
this.saveServices()
|
||||
},
|
||||
saveServices() {
|
||||
localStorage.setItem(servicesStorage, JSON.stringify(this.services))
|
||||
}
|
||||
}
|
||||
})
|
|
@ -0,0 +1,195 @@
|
|||
$avatarSize: 3em
|
||||
$borderRadius: .3em
|
||||
|
||||
$backColor: #333
|
||||
$tileColor: #222
|
||||
$darkColor: #111
|
||||
$halfColor: #999
|
||||
$foreColor: #eee
|
||||
|
||||
@mixin tile
|
||||
margin: $borderRadius
|
||||
background-color: $tileColor
|
||||
border-radius: $borderRadius
|
||||
padding: $borderRadius
|
||||
|
||||
*
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
body
|
||||
background-color: $backColor
|
||||
color: $foreColor
|
||||
|
||||
a
|
||||
text-decoration: none
|
||||
color: #aaa
|
||||
|
||||
.icon
|
||||
width: 1em
|
||||
height: 1em
|
||||
vertical-align: middle
|
||||
|
||||
.text-icon
|
||||
font-weight: bold
|
||||
font-size: 1.2em
|
||||
|
||||
.letter
|
||||
margin: 0 .5em
|
||||
|
||||
.colored
|
||||
color: orange
|
||||
|
||||
#errors
|
||||
position: absolute
|
||||
.error
|
||||
@include tile
|
||||
|
||||
#manager
|
||||
position: absolute
|
||||
bottom: 0
|
||||
|
||||
#services
|
||||
height: 100vh
|
||||
overflow: hidden
|
||||
display: flex
|
||||
& > div
|
||||
flex: 1
|
||||
|
||||
.mastodon
|
||||
.header, .settings, .client .list > div
|
||||
@include tile
|
||||
.header
|
||||
font-size: large
|
||||
text-align: center
|
||||
font-weight: bold
|
||||
.client
|
||||
display: flex
|
||||
height: 100vh
|
||||
overflow: hidden
|
||||
.list
|
||||
height: 100%
|
||||
overflow-y: auto
|
||||
min-height: min-content
|
||||
.statues
|
||||
flex: 1
|
||||
.notifications
|
||||
max-width: 33%
|
||||
|
||||
.account
|
||||
.name
|
||||
margin: 0 $borderRadius
|
||||
color: #eee
|
||||
.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 .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
|
||||
& > .header, .settings
|
||||
@include tile
|
||||
& > .header
|
||||
font-size: large
|
||||
text-align: center
|
||||
font-weight: bold
|
||||
.ic
|
||||
overflow: hidden
|
||||
height: 30px
|
||||
display: inline-block
|
||||
img
|
||||
margin-top: -10px
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
max-width: 30%
|
||||
.list
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
.weather, .forecast
|
||||
flex: 1
|
||||
@include tile
|
||||
.forecast
|
||||
flex: 1
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
max-height: 100%
|
||||
overflow-y: scroll
|
||||
overflow-x: hidden
|
||||
.line
|
||||
flex: 1
|
||||
min-width: 15em
|
||||
@include tile
|
||||
.data
|
||||
float: right
|
||||
.main p
|
||||
display: inline
|
||||
.weather
|
||||
min-width: 17em
|
||||
border: 1px solid $tileColor
|
||||
&.selected
|
||||
border-color: $halfColor
|
||||
.header
|
||||
font-size: 1.2em
|
||||
.data
|
||||
margin-top: .5em
|
||||
.main
|
||||
float: right
|
||||
p
|
||||
margin: $borderRadius
|
||||
display: inline
|
||||
vertical-align: top
|
||||
|
||||
.nextcloud-news
|
||||
& > .header, .settings
|
||||
@include tile
|
||||
& > .header
|
||||
font-size: large
|
||||
text-align: center
|
||||
font-weight: bold
|
||||
.news
|
||||
@include tile
|
||||
.date
|
||||
float: right
|
||||
.read
|
||||
margin-right: .5em
|
||||
.content
|
||||
padding: $borderRadius
|
Loading…
Reference in New Issue