diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2018-02-24 02:46:09 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2018-02-24 02:46:09 +0900 |
| commit | df8a2aea358ca3bcec60c878a6399df46390e3e1 (patch) | |
| tree | 2e187e34a53d9372a797fb9d5882069545f1f03f /src/web/app/common | |
| parent | v3840 (diff) | |
| download | sharkey-df8a2aea358ca3bcec60c878a6399df46390e3e1.tar.gz sharkey-df8a2aea358ca3bcec60c878a6399df46390e3e1.tar.bz2 sharkey-df8a2aea358ca3bcec60c878a6399df46390e3e1.zip | |
Implement #1098
Diffstat (limited to 'src/web/app/common')
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% >></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> |