summaryrefslogtreecommitdiff
path: root/src/web/app/common
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2018-02-24 02:46:09 +0900
committersyuilo <syuilotan@yahoo.co.jp>2018-02-24 02:46:09 +0900
commitdf8a2aea358ca3bcec60c878a6399df46390e3e1 (patch)
tree2e187e34a53d9372a797fb9d5882069545f1f03f /src/web/app/common
parentv3840 (diff)
downloadsharkey-df8a2aea358ca3bcec60c878a6399df46390e3e1.tar.gz
sharkey-df8a2aea358ca3bcec60c878a6399df46390e3e1.tar.bz2
sharkey-df8a2aea358ca3bcec60c878a6399df46390e3e1.zip
Implement #1098
Diffstat (limited to 'src/web/app/common')
-rw-r--r--src/web/app/common/define-widget.ts26
-rw-r--r--src/web/app/common/scripts/check-for-update.ts4
-rw-r--r--src/web/app/common/views/components/index.ts30
-rw-r--r--src/web/app/common/views/components/widgets/access-log.vue90
-rw-r--r--src/web/app/common/views/components/widgets/broadcast.vue161
-rw-r--r--src/web/app/common/views/components/widgets/calendar.vue199
-rw-r--r--src/web/app/common/views/components/widgets/donation.vue58
-rw-r--r--src/web/app/common/views/components/widgets/nav.vue31
-rw-r--r--src/web/app/common/views/components/widgets/photo-stream.vue104
-rw-r--r--src/web/app/common/views/components/widgets/profile.vue125
-rw-r--r--src/web/app/common/views/components/widgets/rss.vue93
-rw-r--r--src/web/app/common/views/components/widgets/server.cpu-memory.vue127
-rw-r--r--src/web/app/common/views/components/widgets/server.cpu.vue68
-rw-r--r--src/web/app/common/views/components/widgets/server.disk.vue76
-rw-r--r--src/web/app/common/views/components/widgets/server.info.vue25
-rw-r--r--src/web/app/common/views/components/widgets/server.memory.vue76
-rw-r--r--src/web/app/common/views/components/widgets/server.pie.vue61
-rw-r--r--src/web/app/common/views/components/widgets/server.uptimes.vue46
-rw-r--r--src/web/app/common/views/components/widgets/server.vue93
-rw-r--r--src/web/app/common/views/components/widgets/slideshow.vue153
-rw-r--r--src/web/app/common/views/components/widgets/tips.vue108
-rw-r--r--src/web/app/common/views/components/widgets/version.vue28
22 files changed, 1775 insertions, 7 deletions
diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index fd13a3395b..60cd1969c0 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -8,6 +8,10 @@ export default function<T extends object>(data: {
props: {
widget: {
type: Object
+ },
+ isMobile: {
+ type: Boolean,
+ default: false
}
},
computed: {
@@ -21,6 +25,7 @@ export default function<T extends object>(data: {
};
},
created() {
+ if (this.widget.data == null) this.widget.data = {};
if (this.props) {
Object.keys(this.props).forEach(prop => {
if (this.widget.data.hasOwnProperty(prop)) {
@@ -30,12 +35,21 @@ export default function<T extends object>(data: {
}
this.$watch('props', newProps => {
- (this as any).api('i/update_home', {
- id: this.id,
- data: newProps
- }).then(() => {
- (this as any).os.i.client_settings.home.find(w => w.id == this.id).data = newProps;
- });
+ if (this.isMobile) {
+ (this as any).api('i/update_mobile_home', {
+ id: this.id,
+ data: newProps
+ }).then(() => {
+ (this as any).os.i.client_settings.mobile_home.find(w => w.id == this.id).data = newProps;
+ });
+ } else {
+ (this as any).api('i/update_home', {
+ id: this.id,
+ data: newProps
+ }).then(() => {
+ (this as any).os.i.client_settings.home.find(w => w.id == this.id).data = newProps;
+ });
+ }
}, {
deep: true
});
diff --git a/src/web/app/common/scripts/check-for-update.ts b/src/web/app/common/scripts/check-for-update.ts
index 0855676a42..fe539407da 100644
--- a/src/web/app/common/scripts/check-for-update.ts
+++ b/src/web/app/common/scripts/check-for-update.ts
@@ -9,7 +9,9 @@ export default async function(mios: MiOS) {
// Clear cache (serive worker)
try {
- navigator.serviceWorker.controller.postMessage('clear');
+ if (navigator.serviceWorker.controller) {
+ navigator.serviceWorker.controller.postMessage('clear');
+ }
navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.forEach(registration => registration.unregister());
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index ab0f1767d4..e66a323266 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -21,6 +21,21 @@ import urlPreview from './url-preview.vue';
import twitterSetting from './twitter-setting.vue';
import fileTypeIcon from './file-type-icon.vue';
+//#region widgets
+import wAccessLog from './widgets/access-log.vue';
+import wVersion from './widgets/version.vue';
+import wRss from './widgets/rss.vue';
+import wProfile from './widgets/profile.vue';
+import wServer from './widgets/server.vue';
+import wBroadcast from './widgets/broadcast.vue';
+import wCalendar from './widgets/calendar.vue';
+import wPhotoStream from './widgets/photo-stream.vue';
+import wSlideshow from './widgets/slideshow.vue';
+import wTips from './widgets/tips.vue';
+import wDonation from './widgets/donation.vue';
+import wNav from './widgets/nav.vue';
+//#endregion
+
Vue.component('mk-signin', signin);
Vue.component('mk-signup', signup);
Vue.component('mk-forkit', forkit);
@@ -41,3 +56,18 @@ Vue.component('mk-messaging-room', messagingRoom);
Vue.component('mk-url-preview', urlPreview);
Vue.component('mk-twitter-setting', twitterSetting);
Vue.component('mk-file-type-icon', fileTypeIcon);
+
+//#region widgets
+Vue.component('mkw-nav', wNav);
+Vue.component('mkw-calendar', wCalendar);
+Vue.component('mkw-photo-stream', wPhotoStream);
+Vue.component('mkw-slideshow', wSlideshow);
+Vue.component('mkw-tips', wTips);
+Vue.component('mkw-donation', wDonation);
+Vue.component('mkw-broadcast', wBroadcast);
+Vue.component('mkw-profile', wProfile);
+Vue.component('mkw-server', wServer);
+Vue.component('mkw-rss', wRss);
+Vue.component('mkw-version', wVersion);
+Vue.component('mkw-access-log', wAccessLog);
+//#endregion
diff --git a/src/web/app/common/views/components/widgets/access-log.vue b/src/web/app/common/views/components/widgets/access-log.vue
new file mode 100644
index 0000000000..c810c2d157
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/access-log.vue
@@ -0,0 +1,90 @@
+<template>
+<div class="mkw-access-log">
+ <mk-widget-container :show-header="props.design == 0">
+ <template slot="header">%fa:server%%i18n:desktop.tags.mk-access-log-home-widget.title%</template>
+
+ <div :class="$style.logs" ref="log">
+ <p v-for="req in requests">
+ <span :class="$style.ip" :style="`color:${ req.fg }; background:${ req.bg }`">{{ req.ip }}</span>
+ <b>{{ req.method }}</b>
+ <span>{{ req.path }}</span>
+ </p>
+ </div>
+ </mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+import * as seedrandom from 'seedrandom';
+
+export default define({
+ name: 'broadcast',
+ props: () => ({
+ design: 0
+ })
+}).extend({
+ data() {
+ return {
+ requests: [],
+ connection: null,
+ connectionId: null
+ };
+ },
+ mounted() {
+ this.connection = (this as any).os.streams.requestsStream.getConnection();
+ this.connectionId = (this as any).os.streams.requestsStream.use();
+ this.connection.on('request', this.onRequest);
+ },
+ beforeDestroy() {
+ this.connection.off('request', this.onRequest);
+ (this as any).os.streams.requestsStream.dispose(this.connectionId);
+ },
+ methods: {
+ onRequest(request) {
+ const random = seedrandom(request.ip);
+ const r = Math.floor(random() * 255);
+ const g = Math.floor(random() * 255);
+ const b = Math.floor(random() * 255);
+ const luma = (0.2126 * r) + (0.7152 * g) + (0.0722 * b); // SMPTE C, Rec. 709 weightings
+ request.bg = `rgb(${r}, ${g}, ${b})`;
+ request.fg = luma >= 165 ? '#000' : '#fff';
+
+ this.requests.push(request);
+ if (this.requests.length > 30) this.requests.shift();
+
+ (this.$refs.log as any).scrollTop = (this.$refs.log as any).scrollHeight;
+ },
+ func() {
+ if (this.props.design == 1) {
+ this.props.design = 0;
+ } else {
+ this.props.design++;
+ }
+ }
+ }
+});
+</script>
+
+<style lang="stylus" module>
+.logs
+ max-height 250px
+ overflow auto
+
+ > p
+ margin 0
+ padding 8px
+ font-size 0.8em
+ color #555
+
+ &:nth-child(odd)
+ background rgba(0, 0, 0, 0.025)
+
+ > b
+ margin-right 4px
+
+.ip
+ margin-right 4px
+ padding 0 4px
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/broadcast.vue b/src/web/app/common/views/components/widgets/broadcast.vue
new file mode 100644
index 0000000000..0bb59caf43
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/broadcast.vue
@@ -0,0 +1,161 @@
+<template>
+<div class="mkw-broadcast"
+ :data-found="broadcasts.length != 0"
+ :data-melt="props.design == 1"
+ :data-mobile="isMobile"
+>
+ <div class="icon">
+ <svg height="32" version="1.1" viewBox="0 0 32 32" width="32">
+ <path class="tower" d="M16.04,11.24c1.79,0,3.239-1.45,3.239-3.24S17.83,4.76,16.04,4.76c-1.79,0-3.24,1.45-3.24,3.24 C12.78,9.78,14.24,11.24,16.04,11.24z M16.04,13.84c-0.82,0-1.66-0.2-2.4-0.6L7.34,29.98h2.98l1.72-2h8l1.681,2H24.7L18.42,13.24 C17.66,13.64,16.859,13.84,16.04,13.84z M16.02,14.8l2.02,7.2h-4L16.02,14.8z M12.04,25.98l2-2h4l2,2H12.04z"></path>
+ <path class="wave a" d="M4.66,1.04c-0.508-0.508-1.332-0.508-1.84,0c-1.86,1.92-2.8,4.44-2.8,6.94c0,2.52,0.94,5.04,2.8,6.96 c0.5,0.52,1.32,0.52,1.82,0s0.5-1.36,0-1.88C3.28,11.66,2.6,9.82,2.6,7.98S3.28,4.3,4.64,2.9C5.157,2.391,5.166,1.56,4.66,1.04z"></path>
+ <path class="wave b" d="M9.58,12.22c0.5-0.5,0.5-1.34,0-1.84C8.94,9.72,8.62,8.86,8.62,8s0.32-1.72,0.96-2.38c0.5-0.52,0.5-1.34,0-1.84 C9.346,3.534,9.02,3.396,8.68,3.4c-0.32,0-0.66,0.12-0.9,0.38C6.64,4.94,6.08,6.48,6.08,8s0.58,3.06,1.7,4.22 C8.28,12.72,9.1,12.72,9.58,12.22z"></path>
+ <path class="wave c" d="M22.42,3.78c-0.5,0.5-0.5,1.34,0,1.84c0.641,0.66,0.96,1.52,0.96,2.38s-0.319,1.72-0.96,2.38c-0.5,0.52-0.5,1.34,0,1.84 c0.487,0.497,1.285,0.505,1.781,0.018c0.007-0.006,0.013-0.012,0.02-0.018c1.139-1.16,1.699-2.7,1.699-4.22s-0.561-3.06-1.699-4.22 c-0.494-0.497-1.297-0.5-1.794-0.007C22.424,3.775,22.422,3.778,22.42,3.78z"></path>
+ <path class="wave d" d="M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z"></path>
+ </svg>
+ </div>
+ <p class="fetching" v-if="fetching">%i18n:desktop.tags.mk-broadcast-home-widget.fetching%<mk-ellipsis/></p>
+ <h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:desktop.tags.mk-broadcast-home-widget.no-broadcasts%' : broadcasts[i].title }}</h1>
+ <p v-if="!fetching">
+ <span v-if="broadcasts.length != 0" v-html="broadcasts[i].text"></span>
+ <template v-if="broadcasts.length == 0">%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%</template>
+ </p>
+ <a v-if="broadcasts.length > 1" @click="next">%i18n:desktop.tags.mk-broadcast-home-widget.next% &gt;&gt;</a>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+import { lang } from '../../../../config';
+
+export default define({
+ name: 'broadcast',
+ props: () => ({
+ design: 0
+ })
+}).extend({
+ data() {
+ return {
+ i: 0,
+ fetching: true,
+ broadcasts: []
+ };
+ },
+ mounted() {
+ (this as any).os.getMeta().then(meta => {
+ let broadcasts = [];
+ if (meta.broadcasts) {
+ meta.broadcasts.forEach(broadcast => {
+ if (broadcast[lang]) {
+ broadcasts.push(broadcast[lang]);
+ }
+ });
+ }
+ this.broadcasts = broadcasts;
+ this.fetching = false;
+ });
+ },
+ methods: {
+ next() {
+ if (this.i == this.broadcasts.length - 1) {
+ this.i = 0;
+ } else {
+ this.i++;
+ }
+ },
+ func() {
+ if (this.props.design == 1) {
+ this.props.design = 0;
+ } else {
+ this.props.design++;
+ }
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-broadcast
+ padding 10px
+ border solid 1px #4078c0
+ border-radius 6px
+
+ &[data-melt]
+ border none
+
+ &[data-found]
+ padding-left 50px
+
+ > .icon
+ display block
+
+ &:after
+ content ""
+ display block
+ clear both
+
+ > .icon
+ display none
+ float left
+ margin-left -40px
+
+ > svg
+ fill currentColor
+ color #4078c0
+
+ > .wave
+ opacity 1
+
+ &.a
+ animation wave 20s ease-in-out 2.1s infinite
+ &.b
+ animation wave 20s ease-in-out 2s infinite
+ &.c
+ animation wave 20s ease-in-out 2s infinite
+ &.d
+ animation wave 20s ease-in-out 2.1s infinite
+
+ @keyframes wave
+ 0%
+ opacity 1
+ 1.5%
+ opacity 0
+ 3.5%
+ opacity 0
+ 5%
+ opacity 1
+ 6.5%
+ opacity 0
+ 8.5%
+ opacity 0
+ 10%
+ opacity 1
+
+ > h1
+ margin 0
+ font-size 0.95em
+ font-weight normal
+ color #4078c0
+
+ > p
+ display block
+ z-index 1
+ margin 0
+ font-size 0.7em
+ color #555
+
+ &.fetching
+ text-align center
+
+ a
+ color #555
+ text-decoration underline
+
+ > a
+ display block
+ font-size 0.7em
+
+ &[data-mobile]
+ > p
+ color #fff
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/calendar.vue b/src/web/app/common/views/components/widgets/calendar.vue
new file mode 100644
index 0000000000..bfcbd7f68d
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/calendar.vue
@@ -0,0 +1,199 @@
+<template>
+<div class="mkw-calendar"
+ :data-melt="props.design == 1"
+ :data-special="special"
+ :data-mobile="isMobile"
+>
+ <div class="calendar" :data-is-holiday="isHoliday">
+ <p class="month-and-year">
+ <span class="year">{{ year }}年</span>
+ <span class="month">{{ month }}月</span>
+ </p>
+ <p class="day">{{ day }}日</p>
+ <p class="week-day">{{ weekDay }}曜日</p>
+ </div>
+ <div class="info">
+ <div>
+ <p>今日:<b>{{ dayP.toFixed(1) }}%</b></p>
+ <div class="meter">
+ <div class="val" :style="{ width: `${dayP}%` }"></div>
+ </div>
+ </div>
+ <div>
+ <p>今月:<b>{{ monthP.toFixed(1) }}%</b></p>
+ <div class="meter">
+ <div class="val" :style="{ width: `${monthP}%` }"></div>
+ </div>
+ </div>
+ <div>
+ <p>今年:<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 define from '../../../../common/define-widget';
+export default define({
+ name: 'calendar',
+ props: () => ({
+ design: 0
+ })
+}).extend({
+ data() {
+ return {
+ now: new Date(),
+ year: null,
+ month: null,
+ day: null,
+ weekDay: null,
+ yearP: null,
+ dayP: null,
+ monthP: null,
+ isHoliday: null,
+ special: null,
+ clock: null
+ };
+ },
+ created() {
+ this.tick();
+ this.clock = setInterval(this.tick, 1000);
+ },
+ beforeDestroy() {
+ clearInterval(this.clock);
+ },
+ methods: {
+ func() {
+ if (this.isMobile) return;
+ if (this.props.design == 2) {
+ this.props.design = 0;
+ } else {
+ this.props.design++;
+ }
+ },
+ 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 = ['日', '月', '火', '水', '木', '金', '土'][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;
+
+ this.special =
+ nm == 0 && nd == 1 ? 'on-new-years-day' :
+ false;
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-calendar
+ padding 16px 0
+ color #777
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ &[data-special='on-new-years-day']
+ border-color #ef95a0
+
+ &[data-melt]
+ background transparent
+ border none
+
+ &[data-mobile]
+ border none
+ border-radius 8px
+ box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
+ &:after
+ content ""
+ display block
+ clear both
+
+ > .calendar
+ float left
+ width 60%
+ text-align center
+
+ &[data-is-holiday]
+ > .day
+ color #ef95a0
+
+ > p
+ margin 0
+ line-height 18px
+ font-size 14px
+
+ > span
+ margin 0 4px
+
+ > .day
+ margin 10px 0
+ line-height 32px
+ font-size 28px
+
+ > .info
+ display block
+ float left
+ width 40%
+ padding 0 16px 0 0
+
+ > div
+ margin-bottom 8px
+
+ &:last-child
+ margin-bottom 4px
+
+ > p
+ margin 0 0 2px 0
+ font-size 12px
+ line-height 18px
+ color #888
+
+ > b
+ margin-left 2px
+
+ > .meter
+ width 100%
+ overflow hidden
+ background #eee
+ border-radius 8px
+
+ > .val
+ height 4px
+ background $theme-color
+
+ &:nth-child(1)
+ > .meter > .val
+ background #f7796c
+
+ &:nth-child(2)
+ > .meter > .val
+ background #a1de41
+
+ &:nth-child(3)
+ > .meter > .val
+ background #41ddde
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/donation.vue b/src/web/app/common/views/components/widgets/donation.vue
new file mode 100644
index 0000000000..08aab8ecd1
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/donation.vue
@@ -0,0 +1,58 @@
+<template>
+<div class="mkw-donation" :data-mobile="isMobile">
+ <article>
+ <h1>%fa:heart%%i18n:desktop.tags.mk-donation-home-widget.title%</h1>
+ <p>
+ {{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr(0, '%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('{')) }}
+ <a href="https://syuilo.com">@syuilo</a>
+ {{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr('%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('}') + 1) }}
+ </p>
+ </article>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+ name: 'donation'
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-donation
+ background #fff
+ border solid 1px #ead8bb
+ border-radius 6px
+
+ > article
+ padding 20px
+
+ > h1
+ margin 0 0 5px 0
+ font-size 1em
+ color #888
+
+ > [data-fa]
+ margin-right 0.25em
+
+ > p
+ display block
+ z-index 1
+ margin 0
+ font-size 0.8em
+ color #999
+
+ &[data-mobile]
+ border none
+ background #ead8bb
+ border-radius 8px
+ box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
+ > article
+ > h1
+ color #7b8871
+
+ > p
+ color #777d71
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/nav.vue b/src/web/app/common/views/components/widgets/nav.vue
new file mode 100644
index 0000000000..ce88e587a8
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/nav.vue
@@ -0,0 +1,31 @@
+<template>
+<div class="mkw-nav">
+ <mk-widget-container>
+ <div :class="$style.body">
+ <mk-nav/>
+ </div>
+ </mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+ name: 'nav'
+});
+</script>
+
+<style lang="stylus" module>
+.body
+ padding 16px
+ font-size 12px
+ color #aaa
+ background #fff
+
+ a
+ color #999
+
+ i
+ color #ccc
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/photo-stream.vue b/src/web/app/common/views/components/widgets/photo-stream.vue
new file mode 100644
index 0000000000..dcaa6624dd
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/photo-stream.vue
@@ -0,0 +1,104 @@
+<template>
+<div class="mkw-photo-stream" :class="$style.root" :data-melt="props.design == 2">
+ <mk-widget-container :show-header="props.design == 0" :naked="props.design == 2">
+ <template slot="header">%fa:camera%%i18n:desktop.tags.mk-photo-stream-home-widget.title%</template>
+
+ <p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ <div :class="$style.stream" v-if="!fetching && images.length > 0">
+ <div v-for="image in images" :key="image.id" :class="$style.img" :style="`background-image: url(${image.url}?thumbnail&size=256)`"></div>
+ </div>
+ <p :class="$style.empty" v-if="!fetching && images.length == 0">%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p>
+ </mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+ name: 'photo-stream',
+ props: () => ({
+ design: 0
+ })
+}).extend({
+ data() {
+ return {
+ images: [],
+ fetching: true,
+ connection: null,
+ connectionId: null
+ };
+ },
+ mounted() {
+ this.connection = (this as any).os.stream.getConnection();
+ this.connectionId = (this as any).os.stream.use();
+
+ this.connection.on('drive_file_created', this.onDriveFileCreated);
+
+ (this as any).api('drive/stream', {
+ type: 'image/*',
+ limit: 9
+ }).then(images => {
+ this.images = images;
+ this.fetching = false;
+ });
+ },
+ beforeDestroy() {
+ this.connection.off('drive_file_created', this.onDriveFileCreated);
+ (this as any).os.stream.dispose(this.connectionId);
+ },
+ methods: {
+ onDriveFileCreated(file) {
+ if (/^image\/.+$/.test(file.type)) {
+ this.images.unshift(file);
+ if (this.images.length > 9) this.images.pop();
+ }
+ },
+ func() {
+ if (this.props.design == 2) {
+ this.props.design = 0;
+ } else {
+ this.props.design++;
+ }
+ }
+ }
+});
+</script>
+
+<style lang="stylus" module>
+.root[data-melt]
+ .stream
+ padding 0
+
+ .img
+ border solid 4px transparent
+ border-radius 8px
+
+.stream
+ display -webkit-flex
+ display -moz-flex
+ display -ms-flex
+ display flex
+ justify-content center
+ flex-wrap wrap
+ padding 8px
+
+ .img
+ flex 1 1 33%
+ width 33%
+ height 80px
+ background-position center center
+ background-size cover
+ border solid 2px transparent
+ border-radius 4px
+
+.fetching
+.empty
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > [data-fa]
+ margin-right 4px
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/profile.vue b/src/web/app/common/views/components/widgets/profile.vue
new file mode 100644
index 0000000000..68cf469788
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/profile.vue
@@ -0,0 +1,125 @@
+<template>
+<div class="mkw-profile"
+ :data-compact="props.design == 1 || props.design == 2"
+ :data-melt="props.design == 2"
+>
+ <div class="banner"
+ :style="os.i.banner_url ? `background-image: url(${os.i.banner_url}?thumbnail&size=256)` : ''"
+ title="クリックでバナー編集"
+ @click="os.apis.updateBanner"
+ ></div>
+ <img class="avatar"
+ :src="`${os.i.avatar_url}?thumbnail&size=96`"
+ @click="os.apis.updateAvatar"
+ alt="avatar"
+ title="クリックでアバター編集"
+ v-user-preview="os.i.id"
+ />
+ <router-link class="name" :to="`/${os.i.username}`">{{ os.i.name }}</router-link>
+ <p class="username">@{{ os.i.username }}</p>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+ name: 'profile',
+ props: () => ({
+ design: 0
+ })
+}).extend({
+ methods: {
+ func() {
+ if (this.props.design == 2) {
+ this.props.design = 0;
+ } else {
+ this.props.design++;
+ }
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-profile
+ overflow hidden
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ &[data-compact]
+ > .banner:before
+ content ""
+ display block
+ width 100%
+ height 100%
+ background rgba(0, 0, 0, 0.5)
+
+ > .avatar
+ top ((100px - 58px) / 2)
+ left ((100px - 58px) / 2)
+ border none
+ border-radius 100%
+ box-shadow 0 0 16px rgba(0, 0, 0, 0.5)
+
+ > .name
+ position absolute
+ top 0
+ left 92px
+ margin 0
+ line-height 100px
+ color #fff
+ text-shadow 0 0 8px rgba(0, 0, 0, 0.5)
+
+ > .username
+ display none
+
+ &[data-melt]
+ background transparent !important
+ border none !important
+
+ > .banner
+ visibility hidden
+
+ > .avatar
+ box-shadow none
+
+ > .name
+ color #666
+ text-shadow none
+
+ > .banner
+ height 100px
+ background-color #f5f5f5
+ background-size cover
+ background-position center
+ cursor pointer
+
+ > .avatar
+ display block
+ position absolute
+ top 76px
+ left 16px
+ width 58px
+ height 58px
+ margin 0
+ border solid 3px #fff
+ border-radius 8px
+ vertical-align bottom
+ cursor pointer
+
+ > .name
+ display block
+ margin 10px 0 0 84px
+ line-height 16px
+ font-weight bold
+ color #555
+
+ > .username
+ display block
+ margin 4px 0 8px 84px
+ line-height 16px
+ font-size 0.9em
+ color #999
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/rss.vue b/src/web/app/common/views/components/widgets/rss.vue
new file mode 100644
index 0000000000..e80896bea6
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/rss.vue
@@ -0,0 +1,93 @@
+<template>
+<div class="mkw-rss" :data-mobile="isMobile">
+ <mk-widget-container :show-header="!props.compact">
+ <template slot="header">%fa:rss-square%RSS</template>
+ <button slot="func" title="設定" @click="setting">%fa:cog%</button>
+
+ <p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ <div :class="$style.feed" v-else>
+ <a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a>
+ </div>
+ </mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+ name: 'rss',
+ props: () => ({
+ compact: false
+ })
+}).extend({
+ data() {
+ return {
+ url: 'http://news.yahoo.co.jp/pickup/rss.xml',
+ items: [],
+ fetching: true,
+ clock: null
+ };
+ },
+ mounted() {
+ this.fetch();
+ this.clock = setInterval(this.fetch, 60000);
+ },
+ beforeDestroy() {
+ clearInterval(this.clock);
+ },
+ methods: {
+ func() {
+ this.props.compact = !this.props.compact;
+ },
+ fetch() {
+ fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.url}`, {
+ cache: 'no-cache'
+ }).then(res => {
+ res.json().then(feed => {
+ this.items = feed.items;
+ this.fetching = false;
+ });
+ });
+ },
+ setting() {
+ alert('not implemented yet');
+ }
+ }
+});
+</script>
+
+<style lang="stylus" module>
+.feed
+ padding 12px 16px
+ font-size 0.9em
+
+ > a
+ display block
+ padding 4px 0
+ color #666
+ border-bottom dashed 1px #eee
+
+ &:last-child
+ border-bottom none
+
+.fetching
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > [data-fa]
+ margin-right 4px
+
+&[data-mobile]
+ .feed
+ padding 0
+ font-size 1em
+
+ > a
+ padding 8px 16px
+
+ &:nth-child(even)
+ background #e2e2e2
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/server.cpu-memory.vue b/src/web/app/common/views/components/widgets/server.cpu-memory.vue
new file mode 100644
index 0000000000..d75a142568
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/server.cpu-memory.vue
@@ -0,0 +1,127 @@
+<template>
+<div class="cpu-memory">
+ <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none">
+ <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"/>
+ </mask>
+ </defs>
+ <rect
+ x="-1" y="-1"
+ :width="viewBoxX + 2" :height="viewBoxY + 2"
+ :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 }`" preserveAspectRatio="none">
+ <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"/>
+ </mask>
+ </defs>
+ <rect
+ x="-1" y="-1"
+ :width="viewBoxX + 2" :height="viewBoxY + 2"
+ :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 Vue from 'vue';
+import * as uuid from 'uuid';
+
+export default Vue.extend({
+ props: ['connection'],
+ data() {
+ return {
+ viewBoxX: 50,
+ viewBoxY: 30,
+ stats: [],
+ cpuGradientId: uuid(),
+ cpuMaskId: uuid(),
+ memGradientId: uuid(),
+ memMaskId: uuid(),
+ cpuPolylinePoints: '',
+ memPolylinePoints: '',
+ cpuPolygonPoints: '',
+ memPolygonPoints: '',
+ cpuP: '',
+ memP: ''
+ };
+ },
+ mounted() {
+ this.connection.on('stats', this.onStats);
+ },
+ beforeDestroy() {
+ this.connection.off('stats', this.onStats);
+ },
+ methods: {
+ onStats(stats) {
+ stats.mem.used = stats.mem.total - stats.mem.free;
+ this.stats.push(stats);
+ if (this.stats.length > 50) this.stats.shift();
+
+ this.cpuPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - s.cpu_usage) * this.viewBoxY}`).join(' ');
+ this.memPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.mem.used / s.mem.total)) * this.viewBoxY}`).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.cpuP = (stats.cpu_usage * 100).toFixed(0);
+ this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0);
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.cpu-memory
+ > svg
+ display block
+ padding 10px
+ width 50%
+ float left
+
+ &:first-child
+ padding-right 5px
+
+ &:last-child
+ padding-left 5px
+
+ > text
+ font-size 5px
+ fill rgba(0, 0, 0, 0.55)
+
+ > tspan
+ opacity 0.5
+
+ &:after
+ content ""
+ display block
+ clear both
+</style>
diff --git a/src/web/app/common/views/components/widgets/server.cpu.vue b/src/web/app/common/views/components/widgets/server.cpu.vue
new file mode 100644
index 0000000000..596c856da8
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/server.cpu.vue
@@ -0,0 +1,68 @@
+<template>
+<div class="cpu">
+ <x-pie class="pie" :value="usage"/>
+ <div>
+ <p>%fa:microchip%CPU</p>
+ <p>{{ meta.cpu.cores }} Cores</p>
+ <p>{{ meta.cpu.model }}</p>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XPie from './server.pie.vue';
+
+export default Vue.extend({
+ components: {
+ XPie
+ },
+ props: ['connection', 'meta'],
+ data() {
+ return {
+ usage: 0
+ };
+ },
+ mounted() {
+ this.connection.on('stats', this.onStats);
+ },
+ beforeDestroy() {
+ this.connection.off('stats', this.onStats);
+ },
+ methods: {
+ onStats(stats) {
+ this.usage = stats.cpu_usage;
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.cpu
+ > .pie
+ padding 10px
+ height 100px
+ float left
+
+ > div
+ float left
+ width calc(100% - 100px)
+ padding 10px 10px 10px 0
+
+ > p
+ margin 0
+ font-size 12px
+ color #505050
+
+ &:first-child
+ font-weight bold
+
+ > [data-fa]
+ margin-right 4px
+
+ &:after
+ content ""
+ display block
+ clear both
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/server.disk.vue b/src/web/app/common/views/components/widgets/server.disk.vue
new file mode 100644
index 0000000000..2af1982a96
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/server.disk.vue
@@ -0,0 +1,76 @@
+<template>
+<div class="disk">
+ <x-pie class="pie" :value="usage"/>
+ <div>
+ <p>%fa:R hdd%Storage</p>
+ <p>Total: {{ total | bytes(1) }}</p>
+ <p>Available: {{ available | bytes(1) }}</p>
+ <p>Used: {{ used | bytes(1) }}</p>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XPie from './server.pie.vue';
+
+export default Vue.extend({
+ components: {
+ XPie
+ },
+ props: ['connection'],
+ data() {
+ return {
+ usage: 0,
+ total: 0,
+ used: 0,
+ available: 0
+ };
+ },
+ mounted() {
+ this.connection.on('stats', this.onStats);
+ },
+ beforeDestroy() {
+ this.connection.off('stats', this.onStats);
+ },
+ methods: {
+ onStats(stats) {
+ stats.disk.used = stats.disk.total - stats.disk.free;
+ this.usage = stats.disk.used / stats.disk.total;
+ this.total = stats.disk.total;
+ this.used = stats.disk.used;
+ this.available = stats.disk.available;
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.disk
+ > .pie
+ padding 10px
+ height 100px
+ float left
+
+ > div
+ float left
+ width calc(100% - 100px)
+ padding 10px 10px 10px 0
+
+ > p
+ margin 0
+ font-size 12px
+ color #505050
+
+ &:first-child
+ font-weight bold
+
+ > [data-fa]
+ margin-right 4px
+
+ &:after
+ content ""
+ display block
+ clear both
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/server.info.vue b/src/web/app/common/views/components/widgets/server.info.vue
new file mode 100644
index 0000000000..bed6a1b743
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/server.info.vue
@@ -0,0 +1,25 @@
+<template>
+<div class="info">
+ <p>Maintainer: <b>{{ meta.maintainer }}</b></p>
+ <p>Machine: {{ meta.machine }}</p>
+ <p>Node: {{ meta.node }}</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: ['meta']
+});
+</script>
+
+<style lang="stylus" scoped>
+.info
+ padding 10px 14px
+
+ > p
+ margin 0
+ font-size 12px
+ color #505050
+</style>
diff --git a/src/web/app/common/views/components/widgets/server.memory.vue b/src/web/app/common/views/components/widgets/server.memory.vue
new file mode 100644
index 0000000000..834a62671d
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/server.memory.vue
@@ -0,0 +1,76 @@
+<template>
+<div class="memory">
+ <x-pie class="pie" :value="usage"/>
+ <div>
+ <p>%fa:flask%Memory</p>
+ <p>Total: {{ total | bytes(1) }}</p>
+ <p>Used: {{ used | bytes(1) }}</p>
+ <p>Free: {{ free | bytes(1) }}</p>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XPie from './server.pie.vue';
+
+export default Vue.extend({
+ components: {
+ XPie
+ },
+ props: ['connection'],
+ data() {
+ return {
+ usage: 0,
+ total: 0,
+ used: 0,
+ free: 0
+ };
+ },
+ mounted() {
+ this.connection.on('stats', this.onStats);
+ },
+ beforeDestroy() {
+ this.connection.off('stats', this.onStats);
+ },
+ methods: {
+ onStats(stats) {
+ stats.mem.used = stats.mem.total - stats.mem.free;
+ this.usage = stats.mem.used / stats.mem.total;
+ this.total = stats.mem.total;
+ this.used = stats.mem.used;
+ this.free = stats.mem.free;
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.memory
+ > .pie
+ padding 10px
+ height 100px
+ float left
+
+ > div
+ float left
+ width calc(100% - 100px)
+ padding 10px 10px 10px 0
+
+ > p
+ margin 0
+ font-size 12px
+ color #505050
+
+ &:first-child
+ font-weight bold
+
+ > [data-fa]
+ margin-right 4px
+
+ &:after
+ content ""
+ display block
+ clear both
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/server.pie.vue b/src/web/app/common/views/components/widgets/server.pie.vue
new file mode 100644
index 0000000000..ce2cff1d00
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/server.pie.vue
@@ -0,0 +1,61 @@
+<template>
+<svg 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 Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ value: {
+ type: Number,
+ required: true
+ }
+ },
+ data() {
+ return {
+ r: 0.4
+ };
+ },
+ 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="stylus" scoped>
+svg
+ display block
+ height 100%
+
+ > circle
+ transform-origin center
+ transform rotate(-90deg)
+ transition stroke-dashoffset 0.5s ease
+
+ > text
+ font-size 0.15px
+ fill rgba(0, 0, 0, 0.6)
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/server.uptimes.vue b/src/web/app/common/views/components/widgets/server.uptimes.vue
new file mode 100644
index 0000000000..06713d83ce
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/server.uptimes.vue
@@ -0,0 +1,46 @@
+<template>
+<div class="uptimes">
+ <p>Uptimes</p>
+ <p>Process: {{ process ? process.toFixed(0) : '---' }}s</p>
+ <p>OS: {{ os ? os.toFixed(0) : '---' }}s</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: ['connection'],
+ data() {
+ return {
+ process: 0,
+ os: 0
+ };
+ },
+ mounted() {
+ this.connection.on('stats', this.onStats);
+ },
+ beforeDestroy() {
+ this.connection.off('stats', this.onStats);
+ },
+ methods: {
+ onStats(stats) {
+ this.process = stats.process_uptime;
+ this.os = stats.os_uptime;
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.uptimes
+ padding 10px 14px
+
+ > p
+ margin 0
+ font-size 12px
+ color #505050
+
+ &:first-child
+ font-weight bold
+</style>
diff --git a/src/web/app/common/views/components/widgets/server.vue b/src/web/app/common/views/components/widgets/server.vue
new file mode 100644
index 0000000000..4ebc5767d6
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/server.vue
@@ -0,0 +1,93 @@
+<template>
+<div class="mkw-server">
+ <mk-widget-container :show-header="props.design == 0" :naked="props.design == 2">
+ <template slot="header">%fa:server%%i18n:desktop.tags.mk-server-home-widget.title%</template>
+ <button slot="func" @click="toggle" title="%i18n:desktop.tags.mk-server-home-widget.toggle%">%fa:sort%</button>
+
+ <p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ <template v-if="!fetching">
+ <x-cpu-memory v-show="props.view == 0" :connection="connection"/>
+ <x-cpu v-show="props.view == 1" :connection="connection" :meta="meta"/>
+ <x-memory v-show="props.view == 2" :connection="connection"/>
+ <x-disk v-show="props.view == 3" :connection="connection"/>
+ <x-uptimes v-show="props.view == 4" :connection="connection"/>
+ <x-info v-show="props.view == 5" :connection="connection" :meta="meta"/>
+ </template>
+ </mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+import XCpuMemory from './server.cpu-memory.vue';
+import XCpu from './server.cpu.vue';
+import XMemory from './server.memory.vue';
+import XDisk from './server.disk.vue';
+import XUptimes from './server.uptimes.vue';
+import XInfo from './server.info.vue';
+
+export default define({
+ name: 'server',
+ props: () => ({
+ design: 0,
+ view: 0
+ })
+}).extend({
+ components: {
+ XCpuMemory,
+ XCpu,
+ XMemory,
+ XDisk,
+ XUptimes,
+ XInfo
+ },
+ data() {
+ return {
+ fetching: true,
+ meta: null,
+ connection: null,
+ connectionId: null
+ };
+ },
+ mounted() {
+ (this as any).os.getMeta().then(meta => {
+ this.meta = meta;
+ this.fetching = false;
+ });
+
+ this.connection = (this as any).os.streams.serverStream.getConnection();
+ this.connectionId = (this as any).os.streams.serverStream.use();
+ },
+ beforeDestroy() {
+ (this as any).os.streams.serverStream.dispose(this.connectionId);
+ },
+ methods: {
+ toggle() {
+ if (this.props.view == 5) {
+ this.props.view = 0;
+ } else {
+ this.props.view++;
+ }
+ },
+ func() {
+ if (this.props.design == 2) {
+ this.props.design = 0;
+ } else {
+ this.props.design++;
+ }
+ }
+ }
+});
+</script>
+
+<style lang="stylus" module>
+.fetching
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > [data-fa]
+ margin-right 4px
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/slideshow.vue b/src/web/app/common/views/components/widgets/slideshow.vue
new file mode 100644
index 0000000000..c2f4eb70d3
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/slideshow.vue
@@ -0,0 +1,153 @@
+<template>
+<div class="mkw-slideshow">
+ <div @click="choose">
+ <p v-if="props.folder === undefined">クリックしてフォルダを指定してください</p>
+ <p v-if="props.folder !== undefined && images.length == 0 && !fetching">このフォルダには画像がありません</p>
+ <div ref="slideA" class="slide a"></div>
+ <div ref="slideB" class="slide b"></div>
+ </div>
+ <button @click="resize">%fa:expand%</button>
+</div>
+</template>
+
+<script lang="ts">
+import * as anime from 'animejs';
+import define from '../../../../common/define-widget';
+export default define({
+ name: 'slideshow',
+ props: () => ({
+ folder: undefined,
+ size: 0
+ })
+}).extend({
+ data() {
+ return {
+ images: [],
+ fetching: true,
+ clock: null
+ };
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.applySize();
+ });
+
+ if (this.props.folder !== undefined) {
+ this.fetch();
+ }
+
+ this.clock = setInterval(this.change, 10000);
+ },
+ beforeDestroy() {
+ 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.applySize();
+ },
+ change() {
+ if (this.images.length == 0) return;
+
+ const index = Math.floor(Math.random() * this.images.length);
+ const img = `url(${ this.images[index].url }?thumbnail&size=1024)`;
+
+ (this.$refs.slideB as any).style.backgroundImage = img;
+
+ anime({
+ targets: this.$refs.slideB,
+ opacity: 1,
+ duration: 1000,
+ easing: 'linear',
+ complete: () => {
+ (this.$refs.slideA as any).style.backgroundImage = img;
+ anime({
+ targets: this.$refs.slideB,
+ opacity: 0,
+ duration: 0
+ });
+ }
+ });
+ },
+ fetch() {
+ this.fetching = true;
+
+ (this as any).api('drive/files', {
+ folder_id: this.props.folder,
+ 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() {
+ (this as any).apis.chooseDriveFolder().then(folder => {
+ this.props.folder = folder ? folder.id : null;
+ this.fetch();
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-slideshow
+ overflow hidden
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ &:hover > button
+ display block
+
+ > button
+ position absolute
+ left 0
+ bottom 0
+ display none
+ padding 4px
+ font-size 24px
+ color #fff
+ text-shadow 0 0 8px #000
+
+ > div
+ width 100%
+ height 100%
+ cursor pointer
+
+ > *
+ pointer-events none
+
+ > .slide
+ position absolute
+ top 0
+ left 0
+ width 100%
+ height 100%
+ background-size cover
+ background-position center
+
+ &.b
+ opacity 0
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/tips.vue b/src/web/app/common/views/components/widgets/tips.vue
new file mode 100644
index 0000000000..2991fbc3b9
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/tips.vue
@@ -0,0 +1,108 @@
+<template>
+<div class="mkw-tips">
+ <p ref="tip">%fa:R lightbulb%<span v-html="tip"></span></p>
+</div>
+</template>
+
+<script lang="ts">
+import * as anime from 'animejs';
+import define from '../../../../common/define-widget';
+
+const tips = [
+ '<kbd>t</kbd>でタイムラインにフォーカスできます',
+ '<kbd>p</kbd>または<kbd>n</kbd>で投稿フォームを開きます',
+ '投稿フォームにはファイルをドラッグ&ドロップできます',
+ '投稿フォームにクリップボードにある画像データをペーストできます',
+ 'ドライブにファイルをドラッグ&ドロップしてアップロードできます',
+ 'ドライブでファイルをドラッグしてフォルダ移動できます',
+ 'ドライブでフォルダをドラッグしてフォルダ移動できます',
+ 'ホームは設定からカスタマイズできます',
+ 'MisskeyはMIT Licenseです',
+ 'タイムマシンウィジェットを利用すると、簡単に過去のタイムラインに遡れます',
+ '投稿の ... をクリックして、投稿をユーザーページにピン留めできます',
+ 'ドライブの容量は(デフォルトで)1GBです',
+ '投稿に添付したファイルは全てドライブに保存されます',
+ 'ホームのカスタマイズ中、ウィジェットを右クリックしてデザインを変更できます',
+ 'タイムライン上部にもウィジェットを設置できます',
+ '投稿をダブルクリックすると詳細が見れます',
+ '「**」でテキストを囲むと**強調表示**されます',
+ 'チャンネルウィジェットを利用すると、よく利用するチャンネルを素早く確認できます',
+ 'いくつかのウィンドウはブラウザの外に切り離すことができます',
+ 'カレンダーウィジェットのパーセンテージは、経過の割合を示しています',
+ 'APIを利用してbotの開発なども行えます',
+ 'MisskeyはLINEを通じてでも利用できます',
+ 'まゆかわいいよまゆ',
+ 'Misskeyは2014年にサービスを開始しました',
+ '対応ブラウザではMisskeyを開いていなくても通知を受け取れます'
+]
+
+export default define({
+ name: 'tips'
+}).extend({
+ data() {
+ return {
+ tip: null,
+ clock: null
+ };
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.set();
+ });
+
+ this.clock = setInterval(this.change, 20000);
+ },
+ beforeDestroy() {
+ clearInterval(this.clock);
+ },
+ methods: {
+ set() {
+ this.tip = tips[Math.floor(Math.random() * tips.length)];
+ },
+ change() {
+ anime({
+ targets: this.$refs.tip,
+ opacity: 0,
+ duration: 500,
+ easing: 'linear',
+ complete: this.set
+ });
+
+ setTimeout(() => {
+ anime({
+ targets: this.$refs.tip,
+ opacity: 1,
+ duration: 500,
+ easing: 'linear'
+ });
+ }, 500);
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-tips
+ overflow visible !important
+
+ > p
+ display block
+ margin 0
+ padding 0 12px
+ text-align center
+ font-size 0.7em
+ color #999
+
+ > [data-fa]
+ margin-right 4px
+
+ kbd
+ display inline
+ padding 0 6px
+ margin 0 2px
+ font-size 1em
+ font-family inherit
+ border solid 1px #999
+ border-radius 2px
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/version.vue b/src/web/app/common/views/components/widgets/version.vue
new file mode 100644
index 0000000000..ad2b27bc40
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/version.vue
@@ -0,0 +1,28 @@
+<template>
+<p>ver {{ v }} (葵 aoi)</p>
+</template>
+
+<script lang="ts">
+import { version } from '../../../../config';
+import define from '../../../../common/define-widget';
+export default define({
+ name: 'version'
+}).extend({
+ data() {
+ return {
+ v: version
+ };
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+p
+ display block
+ margin 0
+ padding 0 12px
+ text-align center
+ font-size 0.7em
+ color #aaa
+
+</style>