refactoring

Resolve #7779
This commit is contained in:
syuilo
2021-11-12 02:02:25 +09:00
parent 037837b551
commit 0e4a111f81
1714 changed files with 20803 additions and 11751 deletions

View File

@ -0,0 +1,85 @@
<template>
<svg viewBox="0 0 21 7">
<rect v-for="record in data" class="day"
width="1" height="1"
:x="record.x" :y="record.date.weekday"
rx="1" ry="1"
fill="transparent">
<title>{{ record.date.year }}/{{ record.date.month + 1 }}/{{ record.date.day }}</title>
</rect>
<rect v-for="record in data" class="day"
:width="record.v" :height="record.v"
:x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)"
rx="1" ry="1"
:fill="record.color"
style="pointer-events: none;"/>
<rect class="today"
width="1" height="1"
:x="data[0].x" :y="data[0].date.weekday"
rx="1" ry="1"
fill="none"
stroke-width="0.1"
stroke="#f73520"/>
</svg>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import * as os from '@/os';
export default defineComponent({
props: ['data'],
created() {
for (const d of this.data) {
d.total = d.notes + d.replies + d.renotes;
}
const peak = Math.max.apply(null, this.data.map(d => d.total));
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const day = now.getDate();
let x = 20;
this.data.slice().forEach((d, i) => {
d.x = x;
const date = new Date(year, month, day - i);
d.date = {
year: date.getFullYear(),
month: date.getMonth(),
day: date.getDate(),
weekday: date.getDay()
};
d.v = peak === 0 ? 0 : d.total / (peak / 2);
if (d.v > 1) d.v = 1;
const ch = d.date.weekday === 0 || d.date.weekday === 6 ? 275 : 170;
const cs = d.v * 100;
const cl = 15 + ((1 - d.v) * 80);
d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
if (d.date.weekday === 0) x--;
});
}
});
</script>
<style lang="scss" scoped>
svg {
display: block;
padding: 16px;
width: 100%;
box-sizing: border-box;
> rect {
transform-origin: center;
&.day {
&:hover {
fill: rgba(#000, 0.05);
}
}
}
}
</style>

View File

@ -0,0 +1,107 @@
<template>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown">
<polyline
:points="pointsNote"
fill="none"
stroke-width="1"
stroke="#41ddde"/>
<polyline
:points="pointsReply"
fill="none"
stroke-width="1"
stroke="#f7796c"/>
<polyline
:points="pointsRenote"
fill="none"
stroke-width="1"
stroke="#a1de41"/>
<polyline
:points="pointsTotal"
fill="none"
stroke-width="1"
stroke="#555"
stroke-dasharray="2 2"/>
</svg>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import * as os from '@/os';
function dragListen(fn) {
window.addEventListener('mousemove', fn);
window.addEventListener('mouseleave', dragClear.bind(null, fn));
window.addEventListener('mouseup', dragClear.bind(null, fn));
}
function dragClear(fn) {
window.removeEventListener('mousemove', fn);
window.removeEventListener('mouseleave', dragClear);
window.removeEventListener('mouseup', dragClear);
}
export default defineComponent({
props: ['data'],
data() {
return {
viewBoxX: 147,
viewBoxY: 60,
zoom: 1,
pos: 0,
pointsNote: null,
pointsReply: null,
pointsRenote: null,
pointsTotal: null
};
},
created() {
for (const d of this.data) {
d.total = d.notes + d.replies + d.renotes;
}
this.render();
},
methods: {
render() {
const peak = Math.max.apply(null, this.data.map(d => d.total));
if (peak != 0) {
const data = this.data.slice().reverse();
this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
this.pointsTotal = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
}
},
onMousedown(e) {
const clickX = e.clientX;
const clickY = e.clientY;
const baseZoom = this.zoom;
const basePos = this.pos;
// 動かした時
dragListen(me => {
let moveLeft = me.clientX - clickX;
let moveTop = me.clientY - clickY;
this.zoom = baseZoom + (-moveTop / 20);
this.pos = basePos + moveLeft;
if (this.zoom < 1) this.zoom = 1;
if (this.pos > 0) this.pos = 0;
if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
this.render();
});
}
}
});
</script>
<style lang="scss" scoped>
svg {
display: block;
padding: 16px;
width: 100%;
box-sizing: border-box;
cursor: all-scroll;
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<MkContainer :show-header="props.showHeader" :naked="props.transparent">
<template #header><i class="fas fa-chart-bar"></i>{{ $ts._widgets.activity }}</template>
<template #func><button @click="toggleView()" class="_button"><i class="fas fa-sort"></i></button></template>
<div>
<MkLoading v-if="fetching"/>
<template v-else>
<XCalendar v-show="props.view === 0" :data="[].concat(activity)"/>
<XChart v-show="props.view === 1" :data="[].concat(activity)"/>
</template>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import define from './define';
import XCalendar from './activity.calendar.vue';
import XChart from './activity.chart.vue';
import * as os from '@/os';
const widget = define({
name: 'activity',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
transparent: {
type: 'boolean',
default: false,
},
view: {
type: 'number',
default: 0,
hidden: true,
},
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer,
XCalendar,
XChart,
},
data() {
return {
fetching: true,
activity: null,
};
},
mounted() {
os.api('charts/user/notes', {
userId: this.$i.id,
span: 'day',
limit: 7 * 21
}).then(activity => {
this.activity = activity.diffs.normal.map((_, i) => ({
total: activity.diffs.normal[i] + activity.diffs.reply[i] + activity.diffs.renote[i],
notes: activity.diffs.normal[i],
replies: activity.diffs.reply[i],
renotes: activity.diffs.renote[i]
}));
this.fetching = false;
});
},
methods: {
toggleView() {
if (this.props.view === 1) {
this.props.view = 0;
} else {
this.props.view++;
}
this.save();
}
}
});
</script>

View File

@ -0,0 +1,59 @@
<template>
<MkContainer :naked="props.transparent" :show-header="false">
<iframe class="dedjhjmo" ref="live2d" @click="touched" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100"></iframe>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import define from './define';
import MkContainer from '@/components/ui/container.vue';
import * as os from '@/os';
const widget = define({
name: 'ai',
props: () => ({
transparent: {
type: 'boolean',
default: false,
},
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer,
},
data() {
return {
};
},
mounted() {
window.addEventListener('mousemove', ev => {
const iframeRect = this.$refs.live2d.getBoundingClientRect();
this.$refs.live2d.contentWindow.postMessage({
type: 'moveCursor',
body: {
x: ev.clientX - iframeRect.left,
y: ev.clientY - iframeRect.top,
}
}, '*');
}, { passive: true });
},
methods: {
touched() {
//if (this.live2d) this.live2d.changeExpression('gurugurume');
}
}
});
</script>
<style lang="scss" scoped>
.dedjhjmo {
width: 100%;
height: 350px;
border: none;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,163 @@
<template>
<MkContainer :show-header="props.showHeader">
<template #header><i class="fas fa-terminal"></i>{{ $ts._widgets.aiscript }}</template>
<div class="uylguesu _monospace">
<textarea v-model="props.script" placeholder="(1 + 1)"></textarea>
<button @click="run" class="_buttonPrimary">RUN</button>
<div class="logs">
<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div>
</div>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import define from './define';
import * as os from '@/os';
import { AiScript, parse, utils } from '@syuilo/aiscript';
import { createAiScriptEnv } from '@/scripts/aiscript/api';
const widget = define({
name: 'aiscript',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
script: {
type: 'string',
multiline: true,
default: '(1 + 1)',
hidden: true,
},
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer
},
data() {
return {
logs: [],
};
},
methods: {
async run() {
this.logs = [];
const aiscript = new AiScript(createAiScriptEnv({
storageKey: 'widget',
token: this.$i?.token,
}), {
in: (q) => {
return new Promise(ok => {
os.dialog({
title: q,
input: {}
}).then(({ canceled, result: a }) => {
ok(a);
});
});
},
out: (value) => {
this.logs.push({
id: Math.random(),
text: value.type === 'str' ? value.value : utils.valToString(value),
print: true
});
},
log: (type, params) => {
switch (type) {
case 'end': this.logs.push({
id: Math.random(),
text: utils.valToString(params.val, true),
print: false
}); break;
default: break;
}
}
});
let ast;
try {
ast = parse(this.props.script);
} catch (e) {
os.dialog({
type: 'error',
text: 'Syntax error :('
});
return;
}
try {
await aiscript.exec(ast);
} catch (e) {
os.dialog({
type: 'error',
text: e
});
}
},
}
});
</script>
<style lang="scss" scoped>
.uylguesu {
text-align: right;
> textarea {
display: block;
width: 100%;
max-width: 100%;
min-width: 100%;
padding: 16px;
color: var(--fg);
background: transparent;
border: none;
border-bottom: solid 0.5px var(--divider);
border-radius: 0;
box-sizing: border-box;
font: inherit;
&:focus-visible {
outline: none;
}
}
> button {
display: inline-block;
margin: 8px;
padding: 0 10px;
height: 28px;
outline: none;
border-radius: 4px;
&:disabled {
opacity: 0.7;
cursor: default;
}
}
> .logs {
border-top: solid 0.5px var(--divider);
text-align: left;
padding: 16px;
&:empty {
display: none;
}
> .log {
&:not(.print) {
opacity: 0.7;
}
}
}
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<div class="mkw-button">
<MkButton :primary="props.colored" full @click="run">
{{ props.label }}
</MkButton>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import define from './define';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import { AiScript, parse, utils } from '@syuilo/aiscript';
import { createAiScriptEnv } from '@/scripts/aiscript/api';
const widget = define({
name: 'button',
props: () => ({
label: {
type: 'string',
default: 'BUTTON',
},
colored: {
type: 'boolean',
default: true,
},
script: {
type: 'string',
multiline: true,
default: 'Mk:dialog("hello" "world")',
},
})
});
export default defineComponent({
components: {
MkButton
},
extends: widget,
data() {
return {
};
},
methods: {
async run() {
const aiscript = new AiScript(createAiScriptEnv({
storageKey: 'widget',
token: this.$i?.token,
}), {
in: (q) => {
return new Promise(ok => {
os.dialog({
title: q,
input: {}
}).then(({ canceled, result: a }) => {
ok(a);
});
});
},
out: (value) => {
// nop
},
log: (type, params) => {
// nop
}
});
let ast;
try {
ast = parse(this.props.script);
} catch (e) {
os.dialog({
type: 'error',
text: 'Syntax error :('
});
return;
}
try {
await aiscript.exec(ast);
} catch (e) {
os.dialog({
type: 'error',
text: e
});
}
},
}
});
</script>
<style lang="scss" scoped>
.mkw-button {
}
</style>

View File

@ -0,0 +1,204 @@
<template>
<div class="mkw-calendar" :class="{ _panel: !props.transparent }">
<div class="calendar" :class="{ isHoliday }">
<p class="month-and-year">
<span class="year">{{ $t('yearX', { year }) }}</span>
<span class="month">{{ $t('monthX', { month }) }}</span>
</p>
<p class="day">{{ $t('dayX', { day }) }}</p>
<p class="week-day">{{ weekDay }}</p>
</div>
<div class="info">
<div>
<p>{{ $ts.today }}: <b>{{ dayP.toFixed(1) }}%</b></p>
<div class="meter">
<div class="val" :style="{ width: `${dayP}%` }"></div>
</div>
</div>
<div>
<p>{{ $ts.thisMonth }}: <b>{{ monthP.toFixed(1) }}%</b></p>
<div class="meter">
<div class="val" :style="{ width: `${monthP}%` }"></div>
</div>
</div>
<div>
<p>{{ $ts.thisYear }}: <b>{{ yearP.toFixed(1) }}%</b></p>
<div class="meter">
<div class="val" :style="{ width: `${yearP}%` }"></div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import define from './define';
import * as os from '@/os';
const widget = define({
name: 'calendar',
props: () => ({
transparent: {
type: 'boolean',
default: false,
},
})
});
export default defineComponent({
extends: widget,
data() {
return {
now: new Date(),
year: null,
month: null,
day: null,
weekDay: null,
yearP: null,
dayP: null,
monthP: null,
isHoliday: null,
clock: null
};
},
created() {
this.tick();
this.clock = setInterval(this.tick, 1000);
},
beforeUnmount() {
clearInterval(this.clock);
},
methods: {
tick() {
const now = new Date();
const nd = now.getDate();
const nm = now.getMonth();
const ny = now.getFullYear();
this.year = ny;
this.month = nm + 1;
this.day = nd;
this.weekDay = [
this.$ts._weekday.sunday,
this.$ts._weekday.monday,
this.$ts._weekday.tuesday,
this.$ts._weekday.wednesday,
this.$ts._weekday.thursday,
this.$ts._weekday.friday,
this.$ts._weekday.saturday
][now.getDay()];
const dayNumer = now.getTime() - new Date(ny, nm, nd).getTime();
const dayDenom = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/;
const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime();
const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime();
const yearNumer = now.getTime() - new Date(ny, 0, 1).getTime();
const yearDenom = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime();
this.dayP = dayNumer / dayDenom * 100;
this.monthP = monthNumer / monthDenom * 100;
this.yearP = yearNumer / yearDenom * 100;
this.isHoliday = now.getDay() === 0 || now.getDay() === 6;
}
}
});
</script>
<style lang="scss" scoped>
.mkw-calendar {
padding: 16px 0;
&:after {
content: "";
display: block;
clear: both;
}
> .calendar {
float: left;
width: 60%;
text-align: center;
&.isHoliday {
> .day {
color: #ef95a0;
}
}
> p {
margin: 0;
line-height: 18px;
font-size: 0.9em;
> span {
margin: 0 4px;
}
}
> .day {
margin: 10px 0;
line-height: 32px;
font-size: 1.75em;
}
}
> .info {
display: block;
float: left;
width: 40%;
padding: 0 16px 0 0;
box-sizing: border-box;
> div {
margin-bottom: 8px;
&:last-child {
margin-bottom: 4px;
}
> p {
margin: 0 0 2px 0;
font-size: 0.75em;
line-height: 18px;
opacity: 0.8;
> b {
margin-left: 2px;
}
}
> .meter {
width: 100%;
overflow: hidden;
background: var(--X11);
border-radius: 8px;
> .val {
height: 4px;
transition: width .3s cubic-bezier(0.23, 1, 0.32, 1);
}
}
&:nth-child(1) {
> .meter > .val {
background: #f7796c;
}
}
&:nth-child(2) {
> .meter > .val {
background: #a1de41;
}
}
&:nth-child(3) {
> .meter > .val {
background: #41ddde;
}
}
}
}
}
</style>

View File

@ -0,0 +1,55 @@
<template>
<MkContainer :naked="props.transparent" :show-header="false">
<div class="vubelbmv">
<MkAnalogClock class="clock" :thickness="props.thickness"/>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import define from './define';
import MkContainer from '@/components/ui/container.vue';
import MkAnalogClock from '@/components/analog-clock.vue';
import * as os from '@/os';
const widget = define({
name: 'clock',
props: () => ({
transparent: {
type: 'boolean',
default: false,
},
thickness: {
type: 'radio',
default: 0.1,
options: [{
value: 0.1, label: 'thin'
}, {
value: 0.2, label: 'medium'
}, {
value: 0.3, label: 'thick'
}]
}
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer,
MkAnalogClock
},
});
</script>
<style lang="scss" scoped>
.vubelbmv {
padding: 8px;
> .clock {
height: 150px;
margin: auto;
}
}
</style>

View File

@ -0,0 +1,75 @@
import { defineComponent } from 'vue';
import { throttle } from 'throttle-debounce';
import { Form } from '@/scripts/form';
import * as os from '@/os';
export default function <T extends Form>(data: {
name: string;
props?: () => T;
}) {
return defineComponent({
props: {
widget: {
type: Object,
required: false
},
settingCallback: {
required: false
}
},
emits: ['updateProps'],
data() {
return {
props: this.widget ? JSON.parse(JSON.stringify(this.widget.data)) : {},
save: throttle(3000, () => {
this.$emit('updateProps', this.props);
}),
};
},
computed: {
id(): string {
return this.widget ? this.widget.id : null;
},
},
created() {
this.mergeProps();
this.$watch('props', () => {
this.mergeProps();
}, { deep: true });
if (this.settingCallback) this.settingCallback(this.setting);
},
methods: {
mergeProps() {
if (data.props) {
const defaultProps = data.props();
for (const prop of Object.keys(defaultProps)) {
if (this.props.hasOwnProperty(prop)) continue;
this.props[prop] = defaultProps[prop].default;
}
}
},
async setting() {
const form = data.props();
for (const item of Object.keys(form)) {
form[item].default = this.props[item];
}
const { canceled, result } = await os.form(data.name, form);
if (canceled) return;
for (const key of Object.keys(result)) {
this.props[key] = result[key];
}
this.save();
},
}
});
}

View File

@ -0,0 +1,79 @@
<template>
<div class="mkw-digitalClock _monospace" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }">
<span>
<span v-text="hh"></span>
<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
<span v-text="mm"></span>
<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
<span v-text="ss"></span>
<span :style="{ visibility: showColon ? 'visible' : 'hidden' }" v-if="props.showMs">:</span>
<span v-text="ms" v-if="props.showMs"></span>
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import define from './define';
import * as os from '@/os';
const widget = define({
name: 'digitalClock',
props: () => ({
transparent: {
type: 'boolean',
default: false,
},
fontSize: {
type: 'number',
default: 1.5,
step: 0.1,
},
showMs: {
type: 'boolean',
default: true,
},
})
});
export default defineComponent({
extends: widget,
data() {
return {
clock: null,
hh: null,
mm: null,
ss: null,
ms: null,
showColon: true,
};
},
created() {
this.tick();
this.$watch(() => this.props.showMs, () => {
if (this.clock) clearInterval(this.clock);
this.clock = setInterval(this.tick, this.props.showMs ? 10 : 1000);
}, { immediate: true });
},
beforeUnmount() {
clearInterval(this.clock);
},
methods: {
tick() {
const now = new Date();
this.hh = now.getHours().toString().padStart(2, '0');
this.mm = now.getMinutes().toString().padStart(2, '0');
this.ss = now.getSeconds().toString().padStart(2, '0');
this.ms = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0');
this.showColon = now.getSeconds() % 2 === 0;
}
}
});
</script>
<style lang="scss" scoped>
.mkw-digitalClock {
padding: 16px 0;
text-align: center;
}
</style>

View File

@ -0,0 +1,145 @@
<template>
<MkContainer :show-header="props.showHeader" :foldable="foldable" :scrollable="scrollable">
<template #header><i class="fas fa-globe"></i>{{ $ts._widgets.federation }}</template>
<div class="wbrkwalb">
<MkLoading v-if="fetching"/>
<transition-group tag="div" name="chart" class="instances" v-else>
<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
<img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/>
<div class="body">
<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">{{ instance.host }}</a>
<p>{{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</p>
</div>
<MkMiniChart class="chart" :src="charts[i].requests.received"/>
</div>
</transition-group>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import define from './define';
import MkMiniChart from '@/components/mini-chart.vue';
import * as os from '@/os';
const widget = define({
name: 'federation',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer, MkMiniChart
},
props: {
foldable: {
type: Boolean,
required: false,
default: false
},
scrollable: {
type: Boolean,
required: false,
default: false
},
},
data() {
return {
instances: [],
charts: [],
fetching: true,
};
},
mounted() {
this.fetch();
this.clock = setInterval(this.fetch, 1000 * 60);
},
beforeUnmount() {
clearInterval(this.clock);
},
methods: {
async fetch() {
const instances = await os.api('federation/instances', {
sort: '+lastCommunicatedAt',
limit: 5
});
const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
this.instances = instances;
this.charts = charts;
this.fetching = false;
}
}
});
</script>
<style lang="scss" scoped>
.wbrkwalb {
$bodyTitleHieght: 18px;
$bodyInfoHieght: 16px;
height: (62px + 1px) + (62px + 1px) + (62px + 1px) + (62px + 1px) + 62px;
overflow: hidden;
> .instances {
.chart-move {
transition: transform 1s ease;
}
> .instance {
display: flex;
align-items: center;
padding: 14px 16px;
border-bottom: solid 0.5px var(--divider);
> img {
display: block;
width: ($bodyTitleHieght + $bodyInfoHieght);
height: ($bodyTitleHieght + $bodyInfoHieght);
object-fit: cover;
border-radius: 4px;
margin-right: 8px;
}
> .body {
flex: 1;
overflow: hidden;
font-size: 0.9em;
color: var(--fg);
padding-right: 8px;
> .a {
display: block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: $bodyTitleHieght;
}
> p {
margin: 0;
font-size: 75%;
opacity: 0.7;
line-height: $bodyInfoHieght;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
> .chart {
height: 30px;
}
}
}
}
</style>

View File

@ -0,0 +1,45 @@
import { App, defineAsyncComponent } from 'vue';
export default function(app: App) {
app.component('MkwMemo', defineAsyncComponent(() => import('./memo.vue')));
app.component('MkwNotifications', defineAsyncComponent(() => import('./notifications.vue')));
app.component('MkwTimeline', defineAsyncComponent(() => import('./timeline.vue')));
app.component('MkwCalendar', defineAsyncComponent(() => import('./calendar.vue')));
app.component('MkwRss', defineAsyncComponent(() => import('./rss.vue')));
app.component('MkwTrends', defineAsyncComponent(() => import('./trends.vue')));
app.component('MkwClock', defineAsyncComponent(() => import('./clock.vue')));
app.component('MkwActivity', defineAsyncComponent(() => import('./activity.vue')));
app.component('MkwPhotos', defineAsyncComponent(() => import('./photos.vue')));
app.component('MkwDigitalClock', defineAsyncComponent(() => import('./digital-clock.vue')));
app.component('MkwFederation', defineAsyncComponent(() => import('./federation.vue')));
app.component('MkwPostForm', defineAsyncComponent(() => import('./post-form.vue')));
app.component('MkwSlideshow', defineAsyncComponent(() => import('./slideshow.vue')));
app.component('MkwServerMetric', defineAsyncComponent(() => import('./server-metric/index.vue')));
app.component('MkwOnlineUsers', defineAsyncComponent(() => import('./online-users.vue')));
app.component('MkwJobQueue', defineAsyncComponent(() => import('./job-queue.vue')));
app.component('MkwButton', defineAsyncComponent(() => import('./button.vue')));
app.component('MkwAiscript', defineAsyncComponent(() => import('./aiscript.vue')));
app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
}
export const widgets = [
'memo',
'notifications',
'timeline',
'calendar',
'rss',
'trends',
'clock',
'activity',
'photos',
'digitalClock',
'federation',
'postForm',
'slideshow',
'serverMetric',
'onlineUsers',
'jobQueue',
'button',
'aiscript',
'aichan',
];

View File

@ -0,0 +1,183 @@
<template>
<div class="mkw-jobQueue _monospace" :class="{ _panel: !props.transparent }">
<div class="inbox">
<div class="label">Inbox queue<i v-if="inbox.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
<div class="values">
<div>
<div>Process</div>
<div :class="{ inc: inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(inbox.activeSincePrevTick) }}</div>
</div>
<div>
<div>Active</div>
<div :class="{ inc: inbox.active > prev.inbox.active, dec: inbox.active < prev.inbox.active }">{{ number(inbox.active) }}</div>
</div>
<div>
<div>Delayed</div>
<div :class="{ inc: inbox.delayed > prev.inbox.delayed, dec: inbox.delayed < prev.inbox.delayed }">{{ number(inbox.delayed) }}</div>
</div>
<div>
<div>Waiting</div>
<div :class="{ inc: inbox.waiting > prev.inbox.waiting, dec: inbox.waiting < prev.inbox.waiting }">{{ number(inbox.waiting) }}</div>
</div>
</div>
</div>
<div class="deliver">
<div class="label">Deliver queue<i v-if="deliver.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
<div class="values">
<div>
<div>Process</div>
<div :class="{ inc: deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(deliver.activeSincePrevTick) }}</div>
</div>
<div>
<div>Active</div>
<div :class="{ inc: deliver.active > prev.deliver.active, dec: deliver.active < prev.deliver.active }">{{ number(deliver.active) }}</div>
</div>
<div>
<div>Delayed</div>
<div :class="{ inc: deliver.delayed > prev.deliver.delayed, dec: deliver.delayed < prev.deliver.delayed }">{{ number(deliver.delayed) }}</div>
</div>
<div>
<div>Waiting</div>
<div :class="{ inc: deliver.waiting > prev.deliver.waiting, dec: deliver.waiting < prev.deliver.waiting }">{{ number(deliver.waiting) }}</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import define from './define';
import * as os from '@/os';
import number from '@/filters/number';
import * as sound from '@/scripts/sound';
const widget = define({
name: 'jobQueue',
props: () => ({
transparent: {
type: 'boolean',
default: false,
},
sound: {
type: 'boolean',
default: false,
},
})
});
export default defineComponent({
extends: widget,
data() {
return {
connection: markRaw(os.stream.useChannel('queueStats')),
inbox: {
activeSincePrevTick: 0,
active: 0,
waiting: 0,
delayed: 0,
},
deliver: {
activeSincePrevTick: 0,
active: 0,
waiting: 0,
delayed: 0,
},
prev: {},
sound: sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1)
};
},
created() {
for (const domain of ['inbox', 'deliver']) {
this.prev[domain] = JSON.parse(JSON.stringify(this[domain]));
}
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 1
});
},
beforeUnmount() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
this.connection.dispose();
},
methods: {
onStats(stats) {
for (const domain of ['inbox', 'deliver']) {
this.prev[domain] = JSON.parse(JSON.stringify(this[domain]));
this[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
this[domain].active = stats[domain].active;
this[domain].waiting = stats[domain].waiting;
this[domain].delayed = stats[domain].delayed;
if (this[domain].waiting > 0 && this.props.sound && this.sound.paused) {
this.sound.play();
}
}
},
onStatsLog(statsLog) {
for (const stats of [...statsLog].reverse()) {
this.onStats(stats);
}
},
number
}
});
</script>
<style lang="scss" scoped>
@keyframes warnBlink {
0% { opacity: 1; }
50% { opacity: 0; }
}
.mkw-jobQueue {
font-size: 0.9em;
> div {
padding: 16px;
&:not(:first-child) {
border-top: solid 0.5px var(--divider);
}
> .label {
display: flex;
> .icon {
color: var(--warn);
margin-left: auto;
animation: warnBlink 1s infinite;
}
}
> .values {
display: flex;
> div {
flex: 1;
> div:first-child {
opacity: 0.7;
}
> div:last-child {
&.inc {
color: var(--warn);
}
&.dec {
color: var(--success);
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,106 @@
<template>
<MkContainer :show-header="props.showHeader">
<template #header><i class="fas fa-sticky-note"></i>{{ $ts._widgets.memo }}</template>
<div class="otgbylcu">
<textarea v-model="text" :placeholder="$ts.placeholder" @input="onChange"></textarea>
<button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $ts.save }}</button>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import define from './define';
import * as os from '@/os';
const widget = define({
name: 'memo',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer
},
data() {
return {
text: null,
changed: false,
timeoutId: null,
};
},
created() {
this.text = this.$store.state.memo;
this.$watch(() => this.$store.reactiveState.memo, text => {
this.text = text;
});
},
methods: {
onChange() {
this.changed = true;
clearTimeout(this.timeoutId);
this.timeoutId = setTimeout(this.saveMemo, 1000);
},
saveMemo() {
this.$store.set('memo', this.text);
this.changed = false;
}
}
});
</script>
<style lang="scss" scoped>
.otgbylcu {
padding-bottom: 28px + 16px;
> textarea {
display: block;
width: 100%;
max-width: 100%;
min-width: 100%;
padding: 16px;
color: var(--fg);
background: transparent;
border: none;
border-bottom: solid 0.5px var(--divider);
border-radius: 0;
box-sizing: border-box;
font: inherit;
font-size: 0.9em;
&:focus-visible {
outline: none;
}
}
> button {
display: block;
position: absolute;
bottom: 8px;
right: 8px;
margin: 0;
padding: 0 10px;
height: 28px;
outline: none;
border-radius: 4px;
&:disabled {
opacity: 0.7;
cursor: default;
}
}
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<MkContainer :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true">
<template #header><i class="fas fa-bell"></i>{{ $ts.notifications }}</template>
<template #func><button @click="configure()" class="_button"><i class="fas fa-cog"></i></button></template>
<div>
<XNotifications :include-types="props.includingTypes"/>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import XNotifications from '@/components/notifications.vue';
import define from './define';
import * as os from '@/os';
const widget = define({
name: 'notifications',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
height: {
type: 'number',
default: 300,
},
includingTypes: {
type: 'array',
hidden: true,
default: null,
},
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer,
XNotifications,
},
data() {
return {
};
},
methods: {
configure() {
os.popup(import('@/components/notification-setting-window.vue'), {
includingTypes: this.props.includingTypes,
}, {
done: async (res) => {
const { includingTypes } = res;
this.props.includingTypes = includingTypes;
this.save();
}
}, 'closed');
}
}
});
</script>

View File

@ -0,0 +1,67 @@
<template>
<div class="mkw-onlineUsers" :class="{ _panel: !props.transparent, pad: !props.transparent }">
<I18n v-if="onlineUsersCount" :src="$ts.onlineUsersCount" text-tag="span" class="text">
<template #n><b>{{ onlineUsersCount }}</b></template>
</I18n>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import define from './define';
import * as os from '@/os';
const widget = define({
name: 'onlineUsers',
props: () => ({
transparent: {
type: 'boolean',
default: true,
},
})
});
export default defineComponent({
extends: widget,
data() {
return {
onlineUsersCount: null,
clock: null,
};
},
created() {
this.tick();
this.clock = setInterval(this.tick, 1000 * 15);
},
beforeUnmount() {
clearInterval(this.clock);
},
methods: {
tick() {
os.api('get-online-users-count').then(res => {
this.onlineUsersCount = res.count;
});
}
}
});
</script>
<style lang="scss" scoped>
.mkw-onlineUsers {
text-align: center;
&.pad {
padding: 16px 0;
}
> .text {
::v-deep(b) {
color: #41b781;
}
::v-deep(span) {
opacity: 0.7;
}
}
}
</style>

View File

@ -0,0 +1,113 @@
<template>
<MkContainer :show-header="props.showHeader" :naked="props.transparent" :class="$style.root" :data-transparent="props.transparent ? true : null">
<template #header><i class="fas fa-camera"></i>{{ $ts._widgets.photos }}</template>
<div class="">
<MkLoading v-if="fetching"/>
<div v-else :class="$style.stream">
<div v-for="(image, i) in images" :key="i"
:class="$style.img"
:style="`background-image: url(${thumbnail(image)})`"
></div>
</div>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import define from './define';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import * as os from '@/os';
const widget = define({
name: 'photos',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
transparent: {
type: 'boolean',
default: false,
},
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer,
},
data() {
return {
images: [],
fetching: true,
connection: null,
};
},
mounted() {
this.connection = markRaw(os.stream.useChannel('main'));
this.connection.on('driveFileCreated', this.onDriveFileCreated);
os.api('drive/stream', {
type: 'image/*',
limit: 9
}).then(images => {
this.images = images;
this.fetching = false;
});
},
beforeUnmount() {
this.connection.dispose();
},
methods: {
onDriveFileCreated(file) {
if (/^image\/.+$/.test(file.type)) {
this.images.unshift(file);
if (this.images.length > 9) this.images.pop();
}
},
thumbnail(image: any): string {
return this.$store.state.disableShowingAnimatedImages
? getStaticImageUrl(image.thumbnailUrl)
: image.thumbnailUrl;
},
}
});
</script>
<style lang="scss" module>
.root[data-transparent] {
.stream {
padding: 0;
}
.img {
border: solid 4px transparent;
border-radius: 8px;
}
}
.stream {
display: flex;
justify-content: center;
flex-wrap: wrap;
padding: 8px;
.img {
flex: 1 1 33%;
width: 33%;
height: 80px;
box-sizing: border-box;
background-position: center center;
background-size: cover;
background-clip: content-box;
border: solid 2px transparent;
border-radius: 4px;
}
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<XPostForm class="_panel" :fixed="true" :autofocus="false"/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import XPostForm from '@/components/post-form.vue';
import define from './define';
const widget = define({
name: 'postForm',
props: () => ({
})
});
export default defineComponent({
extends: widget,
components: {
XPostForm,
},
});
</script>

View File

@ -0,0 +1,89 @@
<template>
<MkContainer :show-header="props.showHeader">
<template #header><i class="fas fa-rss-square"></i>RSS</template>
<template #func><button class="_button" @click="setting"><i class="fas fa-cog"></i></button></template>
<div class="ekmkgxbj">
<MkLoading v-if="fetching"/>
<div class="feed" v-else>
<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
</div>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import define from './define';
import * as os from '@/os';
const widget = define({
name: 'rss',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
url: {
type: 'string',
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
},
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer
},
data() {
return {
items: [],
fetching: true,
clock: null,
};
},
mounted() {
this.fetch();
this.clock = setInterval(this.fetch, 60000);
this.$watch(() => this.props.url, this.fetch);
},
beforeUnmount() {
clearInterval(this.clock);
},
methods: {
fetch() {
fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {
}).then(res => {
res.json().then(feed => {
this.items = feed.items;
this.fetching = false;
});
});
},
}
});
</script>
<style lang="scss" scoped>
.ekmkgxbj {
> .feed {
padding: 0;
font-size: 0.9em;
> a {
display: block;
padding: 8px 16px;
color: var(--fg);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&:nth-child(even) {
background: rgba(#000, 0.05);
}
}
}
}
</style>

View File

@ -0,0 +1,174 @@
<template>
<div class="lcfyofjk">
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<defs>
<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
</linearGradient>
<mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
<polygon
:points="cpuPolygonPoints"
fill="#fff"
fill-opacity="0.5"
/>
<polyline
:points="cpuPolylinePoints"
fill="none"
stroke="#fff"
stroke-width="1"
/>
<circle
:cx="cpuHeadX"
:cy="cpuHeadY"
r="1.5"
fill="#fff"
/>
</mask>
</defs>
<rect
x="-2" y="-2"
:width="viewBoxX + 4" :height="viewBoxY + 4"
:style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"
/>
<text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text>
</svg>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<defs>
<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
</linearGradient>
<mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
<polygon
:points="memPolygonPoints"
fill="#fff"
fill-opacity="0.5"
/>
<polyline
:points="memPolylinePoints"
fill="none"
stroke="#fff"
stroke-width="1"
/>
<circle
:cx="memHeadX"
:cy="memHeadY"
r="1.5"
fill="#fff"
/>
</mask>
</defs>
<rect
x="-2" y="-2"
:width="viewBoxX + 4" :height="viewBoxY + 4"
:style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"
/>
<text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text>
</svg>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { v4 as uuid } from 'uuid';
export default defineComponent({
props: {
connection: {
required: true,
},
meta: {
required: true,
}
},
data() {
return {
viewBoxX: 50,
viewBoxY: 30,
stats: [],
cpuGradientId: uuid(),
cpuMaskId: uuid(),
memGradientId: uuid(),
memMaskId: uuid(),
cpuPolylinePoints: '',
memPolylinePoints: '',
cpuPolygonPoints: '',
memPolygonPoints: '',
cpuHeadX: null,
cpuHeadY: null,
memHeadX: null,
memHeadY: null,
cpuP: '',
memP: ''
};
},
mounted() {
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8)
});
},
beforeUnmount() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
},
methods: {
onStats(stats) {
this.stats.push(stats);
if (this.stats.length > 50) this.stats.shift();
const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu) * this.viewBoxY]);
const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.active / this.meta.mem.total)) * this.viewBoxY]);
this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0];
this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1];
this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0];
this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1];
this.cpuP = (stats.cpu * 100).toFixed(0);
this.memP = (stats.mem.active / this.meta.mem.total * 100).toFixed(0);
},
onStatsLog(statsLog) {
for (const stats of [...statsLog].reverse()) {
this.onStats(stats);
}
}
}
});
</script>
<style lang="scss" scoped>
.lcfyofjk {
display: flex;
> svg {
display: block;
padding: 10px;
width: 50%;
&:first-child {
padding-right: 5px;
}
&:last-child {
padding-left: 5px;
}
> text {
font-size: 5px;
fill: currentColor;
> tspan {
opacity: 0.5;
}
}
}
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<div class="vrvdvrys">
<XPie class="pie" :value="usage"/>
<div>
<p><i class="fas fa-microchip"></i>CPU</p>
<p>{{ meta.cpu.cores }} Logical cores</p>
<p>{{ meta.cpu.model }}</p>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import XPie from './pie.vue';
export default defineComponent({
components: {
XPie
},
props: {
connection: {
required: true,
},
meta: {
required: true,
}
},
data() {
return {
usage: 0,
};
},
mounted() {
this.connection.on('stats', this.onStats);
},
beforeUnmount() {
this.connection.off('stats', this.onStats);
},
methods: {
onStats(stats) {
this.usage = stats.cpu;
}
}
});
</script>
<style lang="scss" scoped>
.vrvdvrys {
display: flex;
padding: 16px;
> .pie {
height: 82px;
flex-shrink: 0;
margin-right: 16px;
}
> div {
flex: 1;
> p {
margin: 0;
font-size: 0.8em;
&:first-child {
font-weight: bold;
margin-bottom: 4px;
> i {
margin-right: 4px;
}
}
}
}
}
</style>

View File

@ -0,0 +1,70 @@
<template>
<div class="zbwaqsat">
<XPie class="pie" :value="usage"/>
<div>
<p><i class="fas fa-hdd"></i>Disk</p>
<p>Total: {{ bytes(total, 1) }}</p>
<p>Free: {{ bytes(available, 1) }}</p>
<p>Used: {{ bytes(used, 1) }}</p>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import XPie from './pie.vue';
import bytes from '@/filters/bytes';
export default defineComponent({
components: {
XPie
},
props: {
meta: {
required: true,
}
},
data() {
return {
usage: this.meta.fs.used / this.meta.fs.total,
total: this.meta.fs.total,
used: this.meta.fs.used,
available: this.meta.fs.total - this.meta.fs.used,
};
},
methods: {
bytes
}
});
</script>
<style lang="scss" scoped>
.zbwaqsat {
display: flex;
padding: 16px;
> .pie {
height: 82px;
flex-shrink: 0;
margin-right: 16px;
}
> div {
flex: 1;
> p {
margin: 0;
font-size: 0.8em;
&:first-child {
font-weight: bold;
margin-bottom: 4px;
> i {
margin-right: 4px;
}
}
}
}
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<MkContainer :show-header="props.showHeader" :naked="props.transparent">
<template #header><i class="fas fa-server"></i>{{ $ts._widgets.serverMetric }}</template>
<template #func><button @click="toggleView()" class="_button"><i class="fas fa-sort"></i></button></template>
<div class="mkw-serverMetric" v-if="meta">
<XCpuMemory v-if="props.view === 0" :connection="connection" :meta="meta"/>
<XNet v-if="props.view === 1" :connection="connection" :meta="meta"/>
<XCpu v-if="props.view === 2" :connection="connection" :meta="meta"/>
<XMemory v-if="props.view === 3" :connection="connection" :meta="meta"/>
<XDisk v-if="props.view === 4" :connection="connection" :meta="meta"/>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import define from '../define';
import MkContainer from '@/components/ui/container.vue';
import XCpuMemory from './cpu-mem.vue';
import XNet from './net.vue';
import XCpu from './cpu.vue';
import XMemory from './mem.vue';
import XDisk from './disk.vue';
import * as os from '@/os';
const widget = define({
name: 'serverMetric',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
transparent: {
type: 'boolean',
default: false,
},
view: {
type: 'number',
default: 0,
hidden: true,
},
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer,
XCpuMemory,
XNet,
XCpu,
XMemory,
XDisk,
},
data() {
return {
meta: null,
connection: null,
};
},
created() {
os.api('server-info', {}).then(res => {
this.meta = res;
});
this.connection = markRaw(os.stream.useChannel('serverStats'));
},
unmounted() {
this.connection.dispose();
},
methods: {
toggleView() {
if (this.props.view == 4) {
this.props.view = 0;
} else {
this.props.view++;
}
this.save();
},
}
});
</script>

View File

@ -0,0 +1,85 @@
<template>
<div class="zlxnikvl">
<XPie class="pie" :value="usage"/>
<div>
<p><i class="fas fa-memory"></i>RAM</p>
<p>Total: {{ bytes(total, 1) }}</p>
<p>Used: {{ bytes(used, 1) }}</p>
<p>Free: {{ bytes(free, 1) }}</p>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import XPie from './pie.vue';
import bytes from '@/filters/bytes';
export default defineComponent({
components: {
XPie
},
props: {
connection: {
required: true,
},
meta: {
required: true,
}
},
data() {
return {
usage: 0,
total: 0,
used: 0,
free: 0,
};
},
mounted() {
this.connection.on('stats', this.onStats);
},
beforeUnmount() {
this.connection.off('stats', this.onStats);
},
methods: {
onStats(stats) {
this.usage = stats.mem.active / this.meta.mem.total;
this.total = this.meta.mem.total;
this.used = stats.mem.active;
this.free = this.meta.mem.total - stats.mem.active;
},
bytes
}
});
</script>
<style lang="scss" scoped>
.zlxnikvl {
display: flex;
padding: 16px;
> .pie {
height: 82px;
flex-shrink: 0;
margin-right: 16px;
}
> div {
flex: 1;
> p {
margin: 0;
font-size: 0.8em;
&:first-child {
font-weight: bold;
margin-bottom: 4px;
> i {
margin-right: 4px;
}
}
}
}
}
</style>

View File

@ -0,0 +1,148 @@
<template>
<div class="oxxrhrto">
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<polygon
:points="inPolygonPoints"
fill="#94a029"
fill-opacity="0.5"
/>
<polyline
:points="inPolylinePoints"
fill="none"
stroke="#94a029"
stroke-width="1"
/>
<circle
:cx="inHeadX"
:cy="inHeadY"
r="1.5"
fill="#94a029"
/>
<text x="1" y="5">NET rx <tspan>{{ bytes(inRecent) }}</tspan></text>
</svg>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<polygon
:points="outPolygonPoints"
fill="#ff9156"
fill-opacity="0.5"
/>
<polyline
:points="outPolylinePoints"
fill="none"
stroke="#ff9156"
stroke-width="1"
/>
<circle
:cx="outHeadX"
:cy="outHeadY"
r="1.5"
fill="#ff9156"
/>
<text x="1" y="5">NET tx <tspan>{{ bytes(outRecent) }}</tspan></text>
</svg>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import bytes from '@/filters/bytes';
export default defineComponent({
props: {
connection: {
required: true,
},
meta: {
required: true,
}
},
data() {
return {
viewBoxX: 50,
viewBoxY: 30,
stats: [],
inPolylinePoints: '',
outPolylinePoints: '',
inPolygonPoints: '',
outPolygonPoints: '',
inHeadX: null,
inHeadY: null,
outHeadX: null,
outHeadY: null,
inRecent: 0,
outRecent: 0
};
},
mounted() {
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8)
});
},
beforeUnmount() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
},
methods: {
onStats(stats) {
this.stats.push(stats);
if (this.stats.length > 50) this.stats.shift();
const inPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.rx)));
const outPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.tx)));
const inPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.rx / inPeak)) * this.viewBoxY]);
const outPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.tx / outPeak)) * this.viewBoxY]);
this.inPolylinePoints = inPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.outPolylinePoints = outPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.inPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.inPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
this.outPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.outPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
this.inHeadX = inPolylinePoints[inPolylinePoints.length - 1][0];
this.inHeadY = inPolylinePoints[inPolylinePoints.length - 1][1];
this.outHeadX = outPolylinePoints[outPolylinePoints.length - 1][0];
this.outHeadY = outPolylinePoints[outPolylinePoints.length - 1][1];
this.inRecent = stats.net.rx;
this.outRecent = stats.net.tx;
},
onStatsLog(statsLog) {
for (const stats of [...statsLog].reverse()) {
this.onStats(stats);
}
},
bytes
}
});
</script>
<style lang="scss" scoped>
.oxxrhrto {
display: flex;
> svg {
display: block;
padding: 10px;
width: 50%;
&:first-child {
padding-right: 5px;
}
&:last-child {
padding-left: 5px;
}
> text {
font-size: 5px;
fill: currentColor;
> tspan {
opacity: 0.5;
}
}
}
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<svg class="hsalcinq" viewBox="0 0 1 1" preserveAspectRatio="none">
<circle
:r="r"
cx="50%" cy="50%"
fill="none"
stroke-width="0.1"
stroke="rgba(0, 0, 0, 0.05)"
/>
<circle
:r="r"
cx="50%" cy="50%"
:stroke-dasharray="Math.PI * (r * 2)"
:stroke-dashoffset="strokeDashoffset"
fill="none"
stroke-width="0.1"
:stroke="color"
/>
<text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (value * 100).toFixed(0) }}%</text>
</svg>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
value: {
type: Number,
required: true
}
},
data() {
return {
r: 0.45
};
},
computed: {
color(): string {
return `hsl(${180 - (this.value * 180)}, 80%, 70%)`;
},
strokeDashoffset(): number {
return (1 - this.value) * (Math.PI * (this.r * 2));
}
}
});
</script>
<style lang="scss" scoped>
.hsalcinq {
display: block;
height: 100%;
> circle {
transform-origin: center;
transform: rotate(-90deg);
transition: stroke-dashoffset 0.5s ease;
}
> text {
font-size: 0.15px;
fill: currentColor;
}
}
</style>

View File

@ -0,0 +1,167 @@
<template>
<div class="kvausudm _panel">
<div @click="choose">
<p v-if="props.folderId == null">
<template v-if="isCustomizeMode">{{ $t('folder-customize-mode') }}</template>
<template v-else>{{ $ts.folder }}</template>
</p>
<p v-if="props.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p>
<div ref="slideA" class="slide a"></div>
<div ref="slideB" class="slide b"></div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import define from './define';
import * as os from '@/os';
const widget = define({
name: 'slideshow',
props: () => ({
height: {
type: 'number',
default: 300,
},
folderId: {
type: 'string',
default: null,
hidden: true,
},
})
});
export default defineComponent({
extends: widget,
data() {
return {
images: [],
fetching: true,
clock: null
};
},
mounted() {
this.$nextTick(() => {
this.applySize();
});
if (this.props.folderId != null) {
this.fetch();
}
this.clock = setInterval(this.change, 10000);
},
beforeUnmount() {
clearInterval(this.clock);
},
methods: {
applySize() {
let h;
if (this.props.size == 1) {
h = 250;
} else {
h = 170;
}
this.$el.style.height = `${h}px`;
},
resize() {
if (this.props.size == 1) {
this.props.size = 0;
} else {
this.props.size++;
}
this.save();
this.applySize();
},
change() {
if (this.images.length == 0) return;
const index = Math.floor(Math.random() * this.images.length);
const img = `url(${ this.images[index].url })`;
(this.$refs.slideB as any).style.backgroundImage = img;
this.$refs.slideB.classList.add('anime');
setTimeout(() => {
// 既にこのウィジェットがunmountされていたら要素がない
if ((this.$refs.slideA as any) == null) return;
(this.$refs.slideA as any).style.backgroundImage = img;
this.$refs.slideB.classList.remove('anime');
}, 1000);
},
fetch() {
this.fetching = true;
os.api('drive/files', {
folderId: this.props.folderId,
type: 'image/*',
limit: 100
}).then(images => {
this.images = images;
this.fetching = false;
(this.$refs.slideA as any).style.backgroundImage = '';
(this.$refs.slideB as any).style.backgroundImage = '';
this.change();
});
},
choose() {
os.selectDriveFolder(false).then(folder => {
if (folder == null) {
return;
}
this.props.folderId = folder.id;
this.save();
this.fetch();
});
}
}
});
</script>
<style lang="scss" scoped>
.kvausudm {
position: relative;
> div {
width: 100%;
height: 100%;
cursor: pointer;
> p {
display: block;
margin: 1em;
text-align: center;
color: #888;
}
> * {
pointer-events: none;
}
> .slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
&.b {
opacity: 0;
}
&.anime {
transition: opacity 1s;
opacity: 1;
}
}
}
}
</style>

View File

@ -0,0 +1,116 @@
<template>
<MkContainer :show-header="props.showHeader" :style="`height: ${props.height}px;`" :scrollable="true">
<template #header>
<button @click="choose" class="_button">
<i v-if="props.src === 'home'" class="fas fa-home"></i>
<i v-else-if="props.src === 'local'" class="fas fa-comments"></i>
<i v-else-if="props.src === 'social'" class="fas fa-share-alt"></i>
<i v-else-if="props.src === 'global'" class="fas fa-globe"></i>
<i v-else-if="props.src === 'list'" class="fas fa-list-ul"></i>
<i v-else-if="props.src === 'antenna'" class="fas fa-satellite"></i>
<span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span>
<i :class="menuOpened ? 'fas fa-angle-up' : 'fas fa-angle-down'" style="margin-left: 8px;"></i>
</button>
</template>
<div>
<XTimeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list ? props.list.id : null" :antenna="props.antenna ? props.antenna.id : null"/>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import XTimeline from '@/components/timeline.vue';
import define from './define';
import * as os from '@/os';
const widget = define({
name: 'timeline',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
height: {
type: 'number',
default: 300,
},
src: {
type: 'string',
default: 'home',
hidden: true,
},
list: {
type: 'object',
default: null,
hidden: true,
},
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer,
XTimeline,
},
data() {
return {
menuOpened: false,
};
},
methods: {
async choose(ev) {
this.menuOpened = true;
const [antennas, lists] = await Promise.all([
os.api('antennas/list'),
os.api('users/lists/list')
]);
const antennaItems = antennas.map(antenna => ({
text: antenna.name,
icon: 'fas fa-satellite',
action: () => {
this.props.antenna = antenna;
this.setSrc('antenna');
}
}));
const listItems = lists.map(list => ({
text: list.name,
icon: 'fas fa-list-ul',
action: () => {
this.props.list = list;
this.setSrc('list');
}
}));
os.popupMenu([{
text: this.$ts._timelines.home,
icon: 'fas fa-home',
action: () => { this.setSrc('home') }
}, {
text: this.$ts._timelines.local,
icon: 'fas fa-comments',
action: () => { this.setSrc('local') }
}, {
text: this.$ts._timelines.social,
icon: 'fas fa-share-alt',
action: () => { this.setSrc('social') }
}, {
text: this.$ts._timelines.global,
icon: 'fas fa-globe',
action: () => { this.setSrc('global') }
}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => {
this.menuOpened = false;
});
},
setSrc(src) {
this.props.src = src;
this.save();
},
}
});
</script>

View File

@ -0,0 +1,111 @@
<template>
<MkContainer :show-header="props.showHeader">
<template #header><i class="fas fa-hashtag"></i>{{ $ts._widgets.trends }}</template>
<div class="wbrkwala">
<MkLoading v-if="fetching"/>
<transition-group tag="div" name="chart" class="tags" v-else>
<div v-for="stat in stats" :key="stat.tag">
<div class="tag">
<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
</div>
<MkMiniChart class="chart" :src="stat.chart"/>
</div>
</transition-group>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import define from './define';
import MkMiniChart from '@/components/mini-chart.vue';
import * as os from '@/os';
const widget = define({
name: 'hashtags',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer, MkMiniChart
},
data() {
return {
stats: [],
fetching: true,
};
},
mounted() {
this.fetch();
this.clock = setInterval(this.fetch, 1000 * 60);
},
beforeUnmount() {
clearInterval(this.clock);
},
methods: {
fetch() {
os.api('hashtags/trend').then(stats => {
this.stats = stats;
this.fetching = false;
});
}
}
});
</script>
<style lang="scss" scoped>
.wbrkwala {
height: (62px + 1px) + (62px + 1px) + (62px + 1px) + (62px + 1px) + 62px;
overflow: hidden;
> .tags {
.chart-move {
transition: transform 1s ease;
}
> div {
display: flex;
align-items: center;
padding: 14px 16px;
border-bottom: solid 0.5px var(--divider);
> .tag {
flex: 1;
overflow: hidden;
font-size: 0.9em;
color: var(--fg);
> .a {
display: block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 18px;
}
> p {
margin: 0;
font-size: 75%;
opacity: 0.7;
line-height: 16px;
}
}
> .chart {
height: 30px;
}
}
}
}
</style>