Initial commit

This commit is contained in:
sheychen 2019-04-14 16:52:12 +02:00
commit 1ac0360671
25 changed files with 14119 additions and 0 deletions

0
compiler/dist/.gitkeep vendored Normal file
View File

1
compiler/dist/mastodon/main.js vendored Normal file

File diff suppressed because one or more lines are too long

1
compiler/dist/nextcloud-news/main.js vendored Normal file

File diff suppressed because one or more lines are too long

1
compiler/dist/openweathermap/main.js vendored Normal file

File diff suppressed because one or more lines are too long

6
compiler/entry.js Normal file
View File

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

69
compiler/package.json Normal file
View File

@ -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"
]
}

View File

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

View File

@ -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>

View File

@ -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)
}
}
}

25
compiler/src/example.vue Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 ? '&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")
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>

View File

@ -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
}
}
}

View File

@ -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)") &#128065;
span.title(@click.stop="news.open = !news.open") {{ news.author }} {{ news.title }}
.content(v-if="news.open && news.body") {{ news.body }}
.auth(v-if="!server")
form(@submit.prevent="setServer")
p
label(for="server") Server:
input#server(v-model="newServer" required)
p
label(for="username") Username:
input#username(v-model="newUsername" required)
p
label(for="token") Token:
input#token(v-model="newToken" required)
p
input(type="submit" value="Connect")
</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>

View File

@ -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 }}&nbsp;
img.icon(:src="`https://openweathermap.org/images/flags/${city.sys.country.toLowerCase()}.png`" :alt="city.sys.country" :title="city.sys.country")
.data
| {{ city.main.temp }}°C {{ city.main.humidity }}%
input.weather(v-show="showAdd" placeholder="city id" @keyup.enter="addCity(parseInt($event.target.value))")
.forecast
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>

123
compiler/webpack.config.js Normal file
View File

@ -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
}
})
]
};
};

33
index.html Normal file
View File

@ -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>

1602
lib/axios.js Executable file

File diff suppressed because it is too large Load Diff

10947
lib/vue.full.js Executable file

File diff suppressed because it is too large Load Diff

317
main.css Normal file
View File

@ -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 */

57
main.js Normal file
View File

@ -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))
}
}
})

195
main.sass Normal file
View File

@ -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