refactor: Widgetのcomposition api移行 (#8125)

* wip

* wip

* wip

* wip

* wip

* wip

* fix
This commit is contained in:
syuilo 2022-01-08 20:30:01 +09:00 committed by GitHub
parent faef125b74
commit 0bbde336b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1389 additions and 1221 deletions

View File

@ -10,7 +10,7 @@
<MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton>
</header>
<XDraggable
v-model="_widgets"
v-model="widgets_"
item-key="id"
animation="150"
>
@ -18,7 +18,7 @@
<div class="customize-container">
<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="fas fa-cog"></i></button>
<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="fas fa-times"></i></button>
<component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" @updateProps="updateWidget(element.id, $event)"/>
<component :ref="el => widgetRefs[element.id] = el" :is="`mkw-${element.name}`" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
</div>
</template>
</XDraggable>
@ -28,7 +28,7 @@
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { defineComponent, defineAsyncComponent, reactive, ref, computed } from 'vue';
import { v4 as uuid } from 'uuid';
import MkSelect from '@/components/form/select.vue';
import MkButton from '@/components/ui/button.vue';
@ -54,50 +54,47 @@ export default defineComponent({
emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'],
data() {
return {
widgetAdderSelected: null,
widgetDefs,
settings: {},
setup(props, context) {
const widgetRefs = reactive({});
const configWidget = (id: string) => {
widgetRefs[id].configure();
};
},
const widgetAdderSelected = ref(null);
const addWidget = () => {
if (widgetAdderSelected.value == null) return;
computed: {
_widgets: {
get() {
return this.widgets;
},
set(value) {
this.$emit('updateWidgets', value);
}
}
},
methods: {
configWidget(id) {
this.settings[id]();
},
addWidget() {
if (this.widgetAdderSelected == null) return;
this.$emit('addWidget', {
name: this.widgetAdderSelected,
context.emit('addWidget', {
name: widgetAdderSelected.value,
id: uuid(),
data: {}
data: {},
});
this.widgetAdderSelected = null;
},
widgetAdderSelected.value = null;
};
const removeWidget = (widget) => {
context.emit('removeWidget', widget);
};
const updateWidget = (id, data) => {
context.emit('updateWidget', { id, data });
};
const widgets_ = computed({
get: () => props.widgets,
set: (value) => {
context.emit('updateWidgets', value);
},
});
removeWidget(widget) {
this.$emit('removeWidget', widget);
},
updateWidget(id, data) {
this.$emit('updateWidget', { id, data });
},
}
return {
widgetRefs,
configWidget,
widgetAdderSelected,
widgetDefs,
addWidget,
removeWidget,
updateWidget,
widgets_,
};
},
});
</script>

View File

@ -21,11 +21,39 @@ export type FormItem = {
default: string | null;
hidden?: boolean;
enum: string[];
} | {
label?: string;
type: 'radio';
default: unknown | null;
hidden?: boolean;
options: {
label: string;
value: unknown;
}[];
} | {
label?: string;
type: 'object';
default: Record<string, unknown> | null;
hidden: true;
} | {
label?: string;
type: 'array';
default: unknown[] | null;
hidden?: boolean;
hidden: true;
};
export type Form = Record<string, FormItem>;
type GetItemType<Item extends FormItem> =
Item['type'] extends 'string' ? string :
Item['type'] extends 'number' ? number :
Item['type'] extends 'boolean' ? boolean :
Item['type'] extends 'radio' ? unknown :
Item['type'] extends 'enum' ? string :
Item['type'] extends 'array' ? unknown[] :
Item['type'] extends 'object' ? Record<string, unknown>
: never;
export type GetFormResultType<F extends Form> = {
[P in keyof F]: GetItemType<F[P]>;
};

View File

@ -1,82 +1,89 @@
<template>
<MkContainer :show-header="props.showHeader" :naked="props.transparent">
<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent">
<template #header><i class="fas fa-chart-bar"></i>{{ $ts._widgets.activity }}</template>
<template #func><button class="_button" @click="toggleView()"><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)"/>
<XCalendar v-show="widgetProps.view === 0" :data="[].concat(activity)"/>
<XChart v-show="widgetProps.view === 1" :data="[].concat(activity)"/>
</template>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
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';
import { $i } from '@/account';
const widget = define({
name: 'activity',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
transparent: {
type: 'boolean',
default: false,
},
view: {
type: 'number',
default: 0,
hidden: true,
},
})
const name = 'activity';
const widgetPropsDef = {
showHeader: {
type: 'boolean' as const,
default: true,
},
transparent: {
type: 'boolean' as const,
default: false,
},
view: {
type: 'number' as const,
default: 0,
hidden: true,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure, save } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const activity = ref(null);
const fetching = ref(true);
const toggleView = () => {
if (widgetProps.view === 1) {
widgetProps.view = 0;
} else {
widgetProps.view++;
}
save();
};
os.api('charts/user/notes', {
userId: $i.id,
span: 'day',
limit: 7 * 21,
}).then(res => {
activity.value = res.diffs.normal.map((_, i) => ({
total: res.diffs.normal[i] + res.diffs.reply[i] + res.diffs.renote[i],
notes: res.diffs.normal[i],
replies: res.diffs.reply[i],
renotes: res.diffs.renote[i]
}));
fetching.value = false;
});
export default defineComponent({
components: {
MkContainer,
XCalendar,
XChart,
},
extends: widget,
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();
}
}
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,51 +1,65 @@
<template>
<MkContainer :naked="props.transparent" :show-header="false">
<MkContainer :naked="widgetProps.transparent" :show-header="false">
<iframe ref="live2d" class="dedjhjmo" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100" @click="touched"></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';
<script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
const widget = define({
name: 'ai',
props: () => ({
transparent: {
type: 'boolean',
default: false,
},
})
const name = 'ai';
const widgetPropsDef = {
transparent: {
type: 'boolean' as const,
default: false,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const live2d = ref<HTMLIFrameElement>();
const touched = () => {
//if (this.live2d) this.live2d.changeExpression('gurugurume');
};
onMounted(() => {
const onMousemove = (ev: MouseEvent) => {
const iframeRect = live2d.value.getBoundingClientRect();
live2d.value.contentWindow.postMessage({
type: 'moveCursor',
body: {
x: ev.clientX - iframeRect.left,
y: ev.clientY - iframeRect.top,
}
}, '*');
};
window.addEventListener('mousemove', onMousemove, { passive: true });
onUnmounted(() => {
window.removeEventListener('mousemove', onMousemove);
});
});
export default defineComponent({
components: {
MkContainer,
},
extends: widget,
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');
}
}
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,9 +1,9 @@
<template>
<MkContainer :show-header="props.showHeader">
<MkContainer :show-header="widgetProps.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>
<textarea v-model="widgetProps.script" placeholder="(1 + 1)"></textarea>
<button class="_buttonPrimary" @click="run">RUN</button>
<div class="logs">
<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
@ -12,97 +12,109 @@
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import define from './define';
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, watch } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
import MkContainer from '@/components/ui/container.vue';
import { AiScript, parse, utils } from '@syuilo/aiscript';
import { createAiScriptEnv } from '@/scripts/aiscript/api';
import { $i } from '@/account';
const widget = define({
name: 'aiscript',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
script: {
type: 'string',
multiline: true,
default: '(1 + 1)',
hidden: true,
},
})
});
const name = 'aiscript';
export default defineComponent({
components: {
MkContainer
const widgetPropsDef = {
showHeader: {
type: 'boolean' as const,
default: true,
},
extends: widget,
data() {
return {
logs: [],
};
script: {
type: 'string' as const,
multiline: true,
default: '(1 + 1)',
hidden: true,
},
};
methods: {
async run() {
this.logs = [];
const aiscript = new AiScript(createAiScriptEnv({
storageKey: 'widget',
token: this.$i?.token,
}), {
in: (q) => {
return new Promise(ok => {
os.inputText({
title: q,
}).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;
}
}
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const logs = ref<{
id: string;
text: string;
print: boolean;
}[]>([]);
const run = async () => {
logs.value = [];
const aiscript = new AiScript(createAiScriptEnv({
storageKey: 'widget',
token: $i?.token,
}), {
in: (q) => {
return new Promise(ok => {
os.inputText({
title: q,
}).then(({ canceled, result: a }) => {
ok(a);
});
});
let ast;
try {
ast = parse(this.props.script);
} catch (e) {
os.alert({
type: 'error',
text: 'Syntax error :('
});
return;
}
try {
await aiscript.exec(ast);
} catch (e) {
os.alert({
type: 'error',
text: e
});
}
},
out: (value) => {
logs.value.push({
id: Math.random().toString(),
text: value.type === 'str' ? value.value : utils.valToString(value),
print: true,
});
},
log: (type, params) => {
switch (type) {
case 'end': logs.value.push({
id: Math.random().toString(),
text: utils.valToString(params.val, true),
print: false,
}); break;
default: break;
}
}
});
let ast;
try {
ast = parse(widgetProps.script);
} catch (e) {
os.alert({
type: 'error',
text: 'Syntax error :(',
});
return;
}
try {
await aiscript.exec(ast);
} catch (e) {
os.alert({
type: 'error',
text: e,
});
}
};
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,90 +1,99 @@
<template>
<div class="mkw-button">
<MkButton :primary="props.colored" full @click="run">
{{ props.label }}
<MkButton :primary="widgetProps.colored" full @click="run">
{{ widgetProps.label }}
</MkButton>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import define from './define';
import MkButton from '@/components/ui/button.vue';
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, watch } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
import { AiScript, parse, utils } from '@syuilo/aiscript';
import { createAiScriptEnv } from '@/scripts/aiscript/api';
import { $i } from '@/account';
import MkButton from '@/components/ui/button.vue';
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")',
},
})
});
const name = 'button';
export default defineComponent({
components: {
MkButton
const widgetPropsDef = {
label: {
type: 'string' as const,
default: 'BUTTON',
},
extends: widget,
data() {
return {
};
colored: {
type: 'boolean' as const,
default: true,
},
methods: {
async run() {
const aiscript = new AiScript(createAiScriptEnv({
storageKey: 'widget',
token: this.$i?.token,
}), {
in: (q) => {
return new Promise(ok => {
os.inputText({
title: q,
}).then(({ canceled, result: a }) => {
ok(a);
});
});
},
out: (value) => {
// nop
},
log: (type, params) => {
// nop
}
script: {
type: 'string' as const,
multiline: true,
default: 'Mk:dialog("hello" "world")',
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const run = async () => {
const aiscript = new AiScript(createAiScriptEnv({
storageKey: 'widget',
token: $i?.token,
}), {
in: (q) => {
return new Promise(ok => {
os.inputText({
title: q,
}).then(({ canceled, result: a }) => {
ok(a);
});
});
let ast;
try {
ast = parse(this.props.script);
} catch (e) {
os.alert({
type: 'error',
text: 'Syntax error :('
});
return;
}
try {
await aiscript.exec(ast);
} catch (e) {
os.alert({
type: 'error',
text: e
});
}
},
out: (value) => {
// nop
},
log: (type, params) => {
// nop
}
});
let ast;
try {
ast = parse(widgetProps.script);
} catch (e) {
os.alert({
type: 'error',
text: 'Syntax error :(',
});
return;
}
try {
await aiscript.exec(ast);
} catch (e) {
os.alert({
type: 'error',
text: e,
});
}
};
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="mkw-calendar" :class="{ _panel: !props.transparent }">
<div class="mkw-calendar" :class="{ _panel: !widgetProps.transparent }">
<div class="calendar" :class="{ isHoliday }">
<p class="month-and-year">
<span class="year">{{ $t('yearX', { year }) }}</span>
@ -32,77 +32,87 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import define from './define';
<script lang="ts" setup>
import { onUnmounted, ref } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import { i18n } from '@/i18n';
const widget = define({
name: 'calendar',
props: () => ({
transparent: {
type: 'boolean',
default: false,
},
})
const name = 'calendar';
const widgetPropsDef = {
transparent: {
type: 'boolean' as const,
default: false,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const year = ref(0);
const month = ref(0);
const day = ref(0);
const weekDay = ref('');
const yearP = ref(0);
const monthP = ref(0);
const dayP = ref(0);
const isHoliday = ref(false);
const tick = () => {
const now = new Date();
const nd = now.getDate();
const nm = now.getMonth();
const ny = now.getFullYear();
year.value = ny;
month.value = nm + 1;
day.value = nd;
weekDay.value = [
i18n.locale._weekday.sunday,
i18n.locale._weekday.monday,
i18n.locale._weekday.tuesday,
i18n.locale._weekday.wednesday,
i18n.locale._weekday.thursday,
i18n.locale._weekday.friday,
i18n.locale._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();
dayP.value = dayNumer / dayDenom * 100;
monthP.value = monthNumer / monthDenom * 100;
yearP.value = yearNumer / yearDenom * 100;
isHoliday.value = now.getDay() === 0 || now.getDay() === 6;
};
tick();
const intervalId = setInterval(tick, 1000);
onUnmounted(() => {
clearInterval(intervalId);
});
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;
}
}
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,45 +1,56 @@
<template>
<MkContainer :naked="props.transparent" :show-header="false">
<MkContainer :naked="widgetProps.transparent" :show-header="false">
<div class="vubelbmv">
<MkAnalogClock class="clock" :thickness="props.thickness"/>
<MkAnalogClock class="clock" :thickness="widgetProps.thickness"/>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import define from './define';
<script lang="ts" setup>
import { } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
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'
}]
}
})
});
const name = 'clock';
export default defineComponent({
components: {
MkContainer,
MkAnalogClock
const widgetPropsDef = {
transparent: {
type: 'boolean' as const,
default: false,
},
extends: widget,
thickness: {
type: 'radio' as const,
default: 0.1,
options: [{
value: 0.1, label: 'thin'
}, {
value: 0.2, label: 'medium'
}, {
value: 0.3, label: 'thick'
}],
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

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

@ -1,73 +1,84 @@
<template>
<div class="mkw-digitalClock _monospace" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }">
<div class="mkw-digitalClock _monospace" :class="{ _panel: !widgetProps.transparent }" :style="{ fontSize: `${widgetProps.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 v-if="props.showMs" :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
<span v-if="props.showMs" v-text="ms"></span>
<span v-if="widgetProps.showMs" :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
<span v-if="widgetProps.showMs" v-text="ms"></span>
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import define from './define';
import * as os from '@/os';
<script lang="ts" setup>
import { onUnmounted, ref, watch } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
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,
},
})
const name = 'digitalClock';
const widgetPropsDef = {
transparent: {
type: 'boolean' as const,
default: false,
},
fontSize: {
type: 'number' as const,
default: 1.5,
step: 0.1,
},
showMs: {
type: 'boolean' as const,
default: true,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
let intervalId;
const hh = ref('');
const mm = ref('');
const ss = ref('');
const ms = ref('');
const showColon = ref(true);
const tick = () => {
const now = new Date();
hh.value = now.getHours().toString().padStart(2, '0');
mm.value = now.getMinutes().toString().padStart(2, '0');
ss.value = now.getSeconds().toString().padStart(2, '0');
ms.value = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0');
showColon.value = now.getSeconds() % 2 === 0;
};
tick();
watch(() => widgetProps.showMs, () => {
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(tick, widgetProps.showMs ? 10 : 1000);
}, { immediate: true });
onUnmounted(() => {
clearInterval(intervalId);
});
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;
}
}
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<MkContainer :show-header="props.showHeader" :foldable="foldable" :scrollable="scrollable">
<MkContainer :show-header="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable">
<template #header><i class="fas fa-globe"></i>{{ $ts._widgets.federation }}</template>
<div class="wbrkwalb">
@ -18,66 +18,64 @@
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
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,
},
})
const name = 'federation';
const widgetPropsDef = {
showHeader: {
type: 'boolean' as const,
default: true,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps> & { foldable?: boolean; scrollable?: boolean; }>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; foldable?: boolean; scrollable?: boolean; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const instances = ref([]);
const charts = ref([]);
const fetching = ref(true);
const fetch = async () => {
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' })));
instances.value = instances;
charts.value = charts;
fetching.value = false;
};
onMounted(() => {
fetch();
const intervalId = setInterval(fetch, 1000 * 60);
onUnmounted(() => {
clearInterval(intervalId);
});
});
export default defineComponent({
components: {
MkContainer, MkMiniChart
},
extends: widget,
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;
}
}
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,134 +1,146 @@
<template>
<div class="mkw-jobQueue _monospace" :class="{ _panel: !props.transparent }">
<div class="mkw-jobQueue _monospace" :class="{ _panel: !widgetProps.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="label">Inbox queue<i v-if="current.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 :class="{ inc: current.inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: current.inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(current.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 :class="{ inc: current.inbox.active > prev.inbox.active, dec: current.inbox.active < prev.inbox.active }">{{ number(current.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 :class="{ inc: current.inbox.delayed > prev.inbox.delayed, dec: current.inbox.delayed < prev.inbox.delayed }">{{ number(current.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 :class="{ inc: current.inbox.waiting > prev.inbox.waiting, dec: current.inbox.waiting < prev.inbox.waiting }">{{ number(current.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="label">Deliver queue<i v-if="current.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 :class="{ inc: current.deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: current.deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(current.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 :class="{ inc: current.deliver.active > prev.deliver.active, dec: current.deliver.active < prev.deliver.active }">{{ number(current.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 :class="{ inc: current.deliver.delayed > prev.deliver.delayed, dec: current.deliver.delayed < prev.deliver.delayed }">{{ number(current.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 :class="{ inc: current.deliver.waiting > prev.deliver.waiting, dec: current.deliver.waiting < prev.deliver.waiting }">{{ number(current.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';
<script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import { stream } from '@/stream';
import number from '@/filters/number';
import * as sound from '@/scripts/sound';
import * as os from '@/os';
const widget = define({
name: 'jobQueue',
props: () => ({
transparent: {
type: 'boolean',
default: false,
},
sound: {
type: 'boolean',
default: false,
},
})
const name = 'jobQueue';
const widgetPropsDef = {
transparent: {
type: 'boolean' as const,
default: false,
},
sound: {
type: 'boolean' as const,
default: false,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const connection = stream.useChannel('queueStats');
const current = reactive({
inbox: {
activeSincePrevTick: 0,
active: 0,
waiting: 0,
delayed: 0,
},
deliver: {
activeSincePrevTick: 0,
active: 0,
waiting: 0,
delayed: 0,
},
});
const prev = reactive({} as typeof current);
const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1);
for (const domain of ['inbox', 'deliver']) {
prev[domain] = JSON.parse(JSON.stringify(current[domain]));
}
const onStats = (stats) => {
for (const domain of ['inbox', 'deliver']) {
prev[domain] = JSON.parse(JSON.stringify(current[domain]));
current[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
current[domain].active = stats[domain].active;
current[domain].waiting = stats[domain].waiting;
current[domain].delayed = stats[domain].delayed;
if (current[domain].waiting > 0 && widgetProps.sound && jammedSound.paused) {
jammedSound.play();
}
}
};
const onStatsLog = (statsLog) => {
for (const stats of [...statsLog].reverse()) {
onStats(stats);
}
};
connection.on('stats', onStats);
connection.on('statsLog', onStatsLog);
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 1,
});
export default defineComponent({
extends: widget,
data() {
return {
connection: markRaw(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);
onUnmounted(() => {
connection.off('stats', onStats);
connection.off('statsLog', onStatsLog);
connection.dispose();
});
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
}
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<MkContainer :show-header="props.showHeader">
<MkContainer :show-header="widgetProps.showHeader">
<template #header><i class="fas fa-sticky-note"></i>{{ $ts._widgets.memo }}</template>
<div class="otgbylcu">
@ -9,56 +9,60 @@
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import define from './define';
<script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
import MkContainer from '@/components/ui/container.vue';
import { defaultStore } from '@/store';
const widget = define({
name: 'memo',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
})
const name = 'memo';
const widgetPropsDef = {
showHeader: {
type: 'boolean' as const,
default: true,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const text = ref<string | null>(defaultStore.state.memo);
const changed = ref(false);
let timeoutId;
const saveMemo = () => {
defaultStore.set('memo', text.value);
changed.value = false;
};
const onChange = () => {
changed.value = true;
clearTimeout(timeoutId);
timeoutId = setTimeout(saveMemo, 1000);
};
watch(() => defaultStore.reactiveState.memo, newText => {
text.value = newText.value;
});
export default defineComponent({
components: {
MkContainer
},
extends: widget,
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;
}
}
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,65 +1,68 @@
<template>
<MkContainer :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true">
<MkContainer :style="`height: ${widgetProps.height}px;`" :show-header="widgetProps.showHeader" :scrollable="true">
<template #header><i class="fas fa-bell"></i>{{ $ts.notifications }}</template>
<template #func><button class="_button" @click="configure()"><i class="fas fa-cog"></i></button></template>
<template #func><button class="_button" @click="configureNotification()"><i class="fas fa-cog"></i></button></template>
<div>
<XNotifications :include-types="props.includingTypes"/>
<XNotifications :include-types="widgetProps.includingTypes"/>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
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,
},
})
});
const name = 'notifications';
export default defineComponent({
components: {
MkContainer,
XNotifications,
const widgetPropsDef = {
showHeader: {
type: 'boolean' as const,
default: true,
},
extends: widget,
data() {
return {
};
height: {
type: 'number' as const,
default: 300,
},
includingTypes: {
type: 'array' as const,
hidden: true,
default: null,
},
};
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');
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure, save } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const configureNotification = () => {
os.popup(import('@/components/notification-setting-window.vue'), {
includingTypes: widgetProps.includingTypes,
}, {
done: async (res) => {
const { includingTypes } = res;
widgetProps.includingTypes = includingTypes;
save();
}
}
}, 'closed');
};
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,48 +1,60 @@
<template>
<div class="mkw-onlineUsers" :class="{ _panel: !props.transparent, pad: !props.transparent }">
<div class="mkw-onlineUsers" :class="{ _panel: !widgetProps.transparent, pad: !widgetProps.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';
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
const widget = define({
name: 'onlineUsers',
props: () => ({
transparent: {
type: 'boolean',
default: true,
},
})
const name = 'onlineUsers';
const widgetPropsDef = {
transparent: {
type: 'boolean' as const,
default: true,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const onlineUsersCount = ref(0);
const tick = () => {
os.api('get-online-users-count').then(res => {
onlineUsersCount.value = res.count;
});
};
onMounted(() => {
tick();
const intervalId = setInterval(tick, 1000 * 15);
onUnmounted(() => {
clearInterval(intervalId);
});
});
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;
});
}
}
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<MkContainer :show-header="props.showHeader" :naked="props.transparent" :class="$style.root" :data-transparent="props.transparent ? true : null">
<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null">
<template #header><i class="fas fa-camera"></i>{{ $ts._widgets.photos }}</template>
<div class="">
@ -14,70 +14,77 @@
</MkContainer>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import define from './define';
<script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import { stream } from '@/stream';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import * as os from '@/os';
import { stream } from '@/stream';
import MkContainer from '@/components/ui/container.vue';
import { defaultStore } from '@/store';
const widget = define({
name: 'photos',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
transparent: {
type: 'boolean',
default: false,
},
})
const name = 'photos';
const widgetPropsDef = {
showHeader: {
type: 'boolean' as const,
default: true,
},
transparent: {
type: 'boolean' as const,
default: false,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const connection = stream.useChannel('main');
const images = ref([]);
const fetching = ref(true);
const onDriveFileCreated = (file) => {
if (/^image\/.+$/.test(file.type)) {
images.value.unshift(file);
if (images.value.length > 9) images.value.pop();
}
};
const thumbnail = (image: any): string => {
return defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(image.thumbnailUrl)
: image.thumbnailUrl;
};
os.api('drive/stream', {
type: 'image/*',
limit: 9
}).then(res => {
images.value = res;
fetching.value = false;
});
export default defineComponent({
components: {
MkContainer,
},
extends: widget,
data() {
return {
images: [],
fetching: true,
connection: null,
};
},
mounted() {
this.connection = markRaw(stream.useChannel('main'));
connection.on('driveFileCreated', onDriveFileCreated);
onUnmounted(() => {
connection.dispose();
});
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;
},
}
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -2,22 +2,34 @@
<XPostForm class="_panel" :fixed="true" :autofocus="false"/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import XPostForm from '@/components/post-form.vue';
import define from './define';
const widget = define({
name: 'postForm',
props: () => ({
})
});
const name = 'postForm';
export default defineComponent({
const widgetPropsDef = {
};
components: {
XPostForm,
},
extends: widget,
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,7 +1,7 @@
<template>
<MkContainer :show-header="props.showHeader">
<MkContainer :show-header="widgetProps.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>
<template #func><button class="_button" @click="configure"><i class="fas fa-cog"></i></button></template>
<div class="ekmkgxbj">
<MkLoading v-if="fetching"/>
@ -12,57 +12,66 @@
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import define from './define';
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, watch } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
import MkContainer from '@/components/ui/container.vue';
const widget = define({
name: 'rss',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
url: {
type: 'string',
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
},
})
const name = 'rss';
const widgetPropsDef = {
showHeader: {
type: 'boolean' as const,
default: true,
},
url: {
type: 'string' as const,
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const items = ref([]);
const fetching = ref(true);
const tick = () => {
fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {}).then(res => {
res.json().then(feed => {
items.value = feed.items;
fetching.value = false;
});
});
};
watch(() => widgetProps.url, tick);
onMounted(() => {
tick();
const intervalId = setInterval(tick, 60000);
onUnmounted(() => {
clearInterval(intervalId);
});
});
export default defineComponent({
components: {
MkContainer
},
extends: widget,
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;
});
});
},
}
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,21 +1,22 @@
<template>
<MkContainer :show-header="props.showHeader" :naked="props.transparent">
<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent">
<template #header><i class="fas fa-server"></i>{{ $ts._widgets.serverMetric }}</template>
<template #func><button class="_button" @click="toggleView()"><i class="fas fa-sort"></i></button></template>
<div v-if="meta" class="mkw-serverMetric">
<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"/>
<XCpuMemory v-if="widgetProps.view === 0" :connection="connection" :meta="meta"/>
<XNet v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/>
<XCpu v-else-if="widgetProps.view === 2" :connection="connection" :meta="meta"/>
<XMemory v-else-if="widgetProps.view === 3" :connection="connection" :meta="meta"/>
<XDisk v-else-if="widgetProps.view === 4" :connection="connection" :meta="meta"/>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import define from '../define';
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget';
import MkContainer from '@/components/ui/container.vue';
import XCpuMemory from './cpu-mem.vue';
import XNet from './net.vue';
@ -25,59 +26,61 @@ import XDisk from './disk.vue';
import * as os from '@/os';
import { stream } from '@/stream';
const widget = define({
name: 'serverMetric',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
transparent: {
type: 'boolean',
default: false,
},
view: {
type: 'number',
default: 0,
hidden: true,
},
})
const name = 'serverMetric';
const widgetPropsDef = {
showHeader: {
type: 'boolean' as const,
default: true,
},
transparent: {
type: 'boolean' as const,
default: false,
},
view: {
type: 'number' as const,
default: 0,
hidden: true,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure, save } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const meta = ref(null);
os.api('server-info', {}).then(res => {
meta.value = res;
});
export default defineComponent({
components: {
MkContainer,
XCpuMemory,
XNet,
XCpu,
XMemory,
XDisk,
},
extends: widget,
data() {
return {
meta: null,
connection: null,
};
},
created() {
os.api('server-info', {}).then(res => {
this.meta = res;
});
this.connection = markRaw(stream.useChannel('serverStats'));
},
unmounted() {
this.connection.dispose();
},
methods: {
toggleView() {
if (this.props.view == 4) {
this.props.view = 0;
} else {
this.props.view++;
}
this.save();
},
const toggleView = () => {
if (widgetProps.view == 4) {
widgetProps.view = 0;
} else {
widgetProps.view++;
}
save();
};
const connection = stream.useChannel('serverStats');
onUnmounted(() => {
connection.dispose();
});
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,126 +1,116 @@
<template>
<div class="kvausudm _panel">
<div class="kvausudm _panel" :style="{ height: widgetProps.height + 'px' }">
<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 v-if="widgetProps.folderId == null">
{{ $ts.folder }}
</p>
<p v-if="props.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p>
<p v-if="widgetProps.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';
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
const widget = define({
name: 'slideshow',
props: () => ({
height: {
type: 'number',
default: 300,
},
folderId: {
type: 'string',
default: null,
hidden: true,
},
})
const name = 'slideshow';
const widgetPropsDef = {
height: {
type: 'number' as const,
default: 300,
},
folderId: {
type: 'string' as const,
default: null,
hidden: true,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure, save } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const images = ref([]);
const fetching = ref(true);
const slideA = ref<HTMLElement>();
const slideB = ref<HTMLElement>();
const change = () => {
if (images.value.length == 0) return;
const index = Math.floor(Math.random() * images.value.length);
const img = `url(${ images.value[index].url })`;
slideB.value.style.backgroundImage = img;
slideB.value.classList.add('anime');
setTimeout(() => {
// unmount
if (slideA.value == null) return;
slideA.value.style.backgroundImage = img;
slideB.value.classList.remove('anime');
}, 1000);
};
const fetch = () => {
fetching.value = true;
os.api('drive/files', {
folderId: widgetProps.folderId,
type: 'image/*',
limit: 100
}).then(res => {
images.value = res;
fetching.value = false;
slideA.value.style.backgroundImage = '';
slideB.value.style.backgroundImage = '';
change();
});
};
const choose = () => {
os.selectDriveFolder(false).then(folder => {
if (folder == null) {
return;
}
widgetProps.folderId = folder.id;
save();
fetch();
});
};
onMounted(() => {
if (widgetProps.folderId != null) {
fetch();
}
const intervalId = setInterval(change, 10000);
onUnmounted(() => {
clearInterval(intervalId);
});
});
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();
});
}
}
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,116 +1,129 @@
<template>
<MkContainer :show-header="props.showHeader" :style="`height: ${props.height}px;`" :scrollable="true">
<MkContainer :show-header="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true">
<template #header>
<button class="_button" @click="choose">
<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 v-if="widgetProps.src === 'home'" class="fas fa-home"></i>
<i v-else-if="widgetProps.src === 'local'" class="fas fa-comments"></i>
<i v-else-if="widgetProps.src === 'social'" class="fas fa-share-alt"></i>
<i v-else-if="widgetProps.src === 'global'" class="fas fa-globe"></i>
<i v-else-if="widgetProps.src === 'list'" class="fas fa-list-ul"></i>
<i v-else-if="widgetProps.src === 'antenna'" class="fas fa-satellite"></i>
<span style="margin-left: 8px;">{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : $t('_timelines.' + widgetProps.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"/>
<XTimeline :key="widgetProps.src === 'list' ? `list:${widgetProps.list.id}` : widgetProps.src === 'antenna' ? `antenna:${widgetProps.antenna.id}` : widgetProps.src" :src="widgetProps.src" :list="widgetProps.list ? widgetProps.list.id : null" :antenna="widgetProps.antenna ? widgetProps.antenna.id : null"/>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
import MkContainer from '@/components/ui/container.vue';
import XTimeline from '@/components/timeline.vue';
import define from './define';
import * as os from '@/os';
import { $i } from '@/account';
import { i18n } from '@/i18n';
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,
},
})
});
const name = 'timeline';
export default defineComponent({
components: {
MkContainer,
XTimeline,
const widgetPropsDef = {
showHeader: {
type: 'boolean' as const,
default: true,
},
extends: widget,
data() {
return {
menuOpened: false,
};
height: {
type: 'number' as const,
default: 300,
},
src: {
type: 'string' as const,
default: 'home',
hidden: true,
},
antenna: {
type: 'object' as const,
default: null,
hidden: true,
},
list: {
type: 'object' as const,
default: null,
hidden: true,
},
};
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;
});
},
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
setSrc(src) {
this.props.src = src;
this.save();
},
}
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure, save } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const menuOpened = ref(false);
const setSrc = (src) => {
widgetProps.src = src;
save();
};
const choose = async (ev) => {
menuOpened.value = 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: () => {
widgetProps.antenna = antenna;
setSrc('antenna');
}
}));
const listItems = lists.map(list => ({
text: list.name,
icon: 'fas fa-list-ul',
action: () => {
widgetProps.list = list;
setSrc('list');
}
}));
os.popupMenu([{
text: i18n.locale._timelines.home,
icon: 'fas fa-home',
action: () => { setSrc('home') }
}, {
text: i18n.locale._timelines.local,
icon: 'fas fa-comments',
action: () => { setSrc('local') }
}, {
text: i18n.locale._timelines.social,
icon: 'fas fa-share-alt',
action: () => { setSrc('social') }
}, {
text: i18n.locale._timelines.global,
icon: 'fas fa-globe',
action: () => { setSrc('global') }
}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => {
menuOpened.value = false;
});
};
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<MkContainer :show-header="props.showHeader">
<MkContainer :show-header="widgetProps.showHeader">
<template #header><i class="fas fa-hashtag"></i>{{ $ts._widgets.trends }}</template>
<div class="wbrkwala">
@ -17,49 +17,59 @@
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
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,
},
})
const name = 'hashtags';
const widgetPropsDef = {
showHeader: {
type: 'boolean' as const,
default: true,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
const stats = ref([]);
const fetching = ref(true);
const fetch = () => {
os.api('hashtags/trend').then(stats => {
stats.value = stats;
fetching.value = false;
});
};
onMounted(() => {
fetch();
const intervalId = setInterval(fetch, 1000 * 60);
onUnmounted(() => {
clearInterval(intervalId);
});
});
export default defineComponent({
components: {
MkContainer, MkMiniChart
},
extends: widget,
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;
});
}
}
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@ -0,0 +1,71 @@
import { reactive, watch } from 'vue';
import { throttle } from 'throttle-debounce';
import { Form, GetFormResultType } from '@/scripts/form';
import * as os from '@/os';
export type Widget<P extends Record<string, unknown>> = {
id: string;
data: Partial<P>;
};
export type WidgetComponentProps<P extends Record<string, unknown>> = {
widget?: Widget<P>;
};
export type WidgetComponentEmits<P extends Record<string, unknown>> = {
(e: 'updateProps', props: P);
};
export type WidgetComponentExpose = {
name: string;
id: string | null;
configure: () => void;
};
export const useWidgetPropsManager = <F extends Form & Record<string, { default: any; }>>(
name: string,
propsDef: F,
props: Readonly<WidgetComponentProps<GetFormResultType<F>>>,
emit: WidgetComponentEmits<GetFormResultType<F>>,
): {
widgetProps: GetFormResultType<F>;
save: () => void;
configure: () => void;
} => {
const widgetProps = reactive(props.widget ? JSON.parse(JSON.stringify(props.widget.data)) : {});
const mergeProps = () => {
for (const prop of Object.keys(propsDef)) {
if (widgetProps.hasOwnProperty(prop)) continue;
widgetProps[prop] = propsDef[prop].default;
}
};
watch(widgetProps, () => {
mergeProps();
}, { deep: true, immediate: true, });
const save = throttle(3000, () => {
emit('updateProps', widgetProps)
});
const configure = async () => {
const form = JSON.parse(JSON.stringify(propsDef));
for (const item of Object.keys(form)) {
form[item].default = widgetProps[item];
}
const { canceled, result } = await os.form(name, form);
if (canceled) return;
for (const key of Object.keys(result)) {
widgetProps[key] = result[key];
}
save();
};
return {
widgetProps,
save,
configure,
};
};