This commit is contained in:
syuilo 2018-02-24 02:46:09 +09:00
parent 83f2e906bb
commit df8a2aea35
42 changed files with 823 additions and 511 deletions

View file

@ -182,7 +182,12 @@ const endpoints: Endpoint[] = [
{ {
name: 'i/update_home', name: 'i/update_home',
withCredential: true, withCredential: true,
kind: 'account-write' secure: true
},
{
name: 'i/update_mobile_home',
withCredential: true,
secure: true
}, },
{ {
name: 'i/change_password', name: 'i/change_password',

View file

@ -4,16 +4,7 @@
import $ from 'cafy'; import $ from 'cafy';
import User from '../../models/user'; import User from '../../models/user';
/** module.exports = async (params, user) => new Promise(async (res, rej) => {
* Update myself
*
* @param {any} params
* @param {any} user
* @param {any} _
* @param {boolean} isSecure
* @return {Promise<any>}
*/
module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => {
// Get 'home' parameter // Get 'home' parameter
const [home, homeErr] = $(params.home).optional.array().each( const [home, homeErr] = $(params.home).optional.array().each(
$().strict.object() $().strict.object()

View file

@ -0,0 +1,50 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import User from '../../models/user';
module.exports = async (params, user) => new Promise(async (res, rej) => {
// Get 'home' parameter
const [home, homeErr] = $(params.home).optional.array().each(
$().strict.object()
.have('name', $().string())
.have('id', $().string())
.have('data', $().object())).$;
if (homeErr) return rej('invalid home param');
// Get 'id' parameter
const [id, idErr] = $(params.id).optional.string().$;
if (idErr) return rej('invalid id param');
// Get 'data' parameter
const [data, dataErr] = $(params.data).optional.object().$;
if (dataErr) return rej('invalid data param');
if (home) {
await User.update(user._id, {
$set: {
'client_settings.mobile_home': home
}
});
res();
} else {
if (id == null && data == null) return rej('you need to set id and data params if home param unset');
const _home = user.client_settings.mobile_home || [];
const widget = _home.find(w => w.id == id);
if (widget == null) return rej('widget not found');
widget.data = data;
await User.update(user._id, {
$set: {
'client_settings.mobile_home': _home
}
});
res();
}
});

View file

@ -8,6 +8,10 @@ export default function<T extends object>(data: {
props: { props: {
widget: { widget: {
type: Object type: Object
},
isMobile: {
type: Boolean,
default: false
} }
}, },
computed: { computed: {
@ -21,6 +25,7 @@ export default function<T extends object>(data: {
}; };
}, },
created() { created() {
if (this.widget.data == null) this.widget.data = {};
if (this.props) { if (this.props) {
Object.keys(this.props).forEach(prop => { Object.keys(this.props).forEach(prop => {
if (this.widget.data.hasOwnProperty(prop)) { if (this.widget.data.hasOwnProperty(prop)) {
@ -30,12 +35,21 @@ export default function<T extends object>(data: {
} }
this.$watch('props', newProps => { this.$watch('props', 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', { (this as any).api('i/update_home', {
id: this.id, id: this.id,
data: newProps data: newProps
}).then(() => { }).then(() => {
(this as any).os.i.client_settings.home.find(w => w.id == this.id).data = newProps; (this as any).os.i.client_settings.home.find(w => w.id == this.id).data = newProps;
}); });
}
}, { }, {
deep: true deep: true
}); });

View file

@ -9,7 +9,9 @@ export default async function(mios: MiOS) {
// Clear cache (serive worker) // Clear cache (serive worker)
try { try {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('clear'); navigator.serviceWorker.controller.postMessage('clear');
}
navigator.serviceWorker.getRegistrations().then(registrations => { navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.forEach(registration => registration.unregister()); registrations.forEach(registration => registration.unregister());

View file

@ -21,6 +21,21 @@ import urlPreview from './url-preview.vue';
import twitterSetting from './twitter-setting.vue'; import twitterSetting from './twitter-setting.vue';
import fileTypeIcon from './file-type-icon.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-signin', signin);
Vue.component('mk-signup', signup); Vue.component('mk-signup', signup);
Vue.component('mk-forkit', forkit); Vue.component('mk-forkit', forkit);
@ -41,3 +56,18 @@ Vue.component('mk-messaging-room', messagingRoom);
Vue.component('mk-url-preview', urlPreview); Vue.component('mk-url-preview', urlPreview);
Vue.component('mk-twitter-setting', twitterSetting); Vue.component('mk-twitter-setting', twitterSetting);
Vue.component('mk-file-type-icon', fileTypeIcon); 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

View file

@ -1,15 +1,16 @@
<template> <template>
<div class="mkw-access-log"> <div class="mkw-access-log">
<template v-if="props.design == 0"> <mk-widget-container :show-header="props.design == 0">
<p class="title">%fa:server%%i18n:desktop.tags.mk-access-log-home-widget.title%</p> <template slot="header">%fa:server%%i18n:desktop.tags.mk-access-log-home-widget.title%</template>
</template>
<div ref="log"> <div :class="$style.logs" ref="log">
<p v-for="req in requests"> <p v-for="req in requests">
<span class="ip" :style="`color:${ req.fg }; background:${ req.bg }`">{{ req.ip }}</span> <span :class="$style.ip" :style="`color:${ req.fg }; background:${ req.bg }`">{{ req.ip }}</span>
<b>{{ req.method }}</b> <b>{{ req.method }}</b>
<span>{{ req.path }}</span> <span>{{ req.path }}</span>
</p> </p>
</div> </div>
</mk-widget-container>
</div> </div>
</template> </template>
@ -65,27 +66,8 @@ export default define({
}); });
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" module>
.mkw-access-log .logs
overflow hidden
background #fff
border solid 1px rgba(0, 0, 0, 0.075)
border-radius 6px
> .title
z-index 1
margin 0
padding 0 16px
line-height 42px
font-size 0.9em
font-weight bold
color #888
box-shadow 0 1px rgba(0, 0, 0, 0.07)
> [data-fa]
margin-right 4px
> div
max-height 250px max-height 250px
overflow auto overflow auto
@ -98,11 +80,11 @@ export default define({
&:nth-child(odd) &:nth-child(odd)
background rgba(0, 0, 0, 0.025) background rgba(0, 0, 0, 0.025)
> .ip
margin-right 4px
padding 0 4px
> b > b
margin-right 4px margin-right 4px
.ip
margin-right 4px
padding 0 4px
</style> </style>

View file

@ -1,5 +1,9 @@
<template> <template>
<div class="mkw-broadcast" :data-found="broadcasts.length != 0" :data-melt="props.design == 1"> <div class="mkw-broadcast"
:data-found="broadcasts.length != 0"
:data-melt="props.design == 1"
:data-mobile="isMobile"
>
<div class="icon"> <div class="icon">
<svg height="32" version="1.1" viewBox="0 0 32 32" width="32"> <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="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>
@ -150,4 +154,8 @@ export default define({
display block display block
font-size 0.7em font-size 0.7em
&[data-mobile]
> p
color #fff
</style> </style>

View file

@ -2,6 +2,7 @@
<div class="mkw-calendar" <div class="mkw-calendar"
:data-melt="props.design == 1" :data-melt="props.design == 1"
:data-special="special" :data-special="special"
:data-mobile="isMobile"
> >
<div class="calendar" :data-is-holiday="isHoliday"> <div class="calendar" :data-is-holiday="isHoliday">
<p class="month-and-year"> <p class="month-and-year">
@ -66,6 +67,7 @@ export default define({
}, },
methods: { methods: {
func() { func() {
if (this.isMobile) return;
if (this.props.design == 2) { if (this.props.design == 2) {
this.props.design = 0; this.props.design = 0;
} else { } else {
@ -119,6 +121,11 @@ export default define({
background transparent background transparent
border none border none
&[data-mobile]
border none
border-radius 8px
box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
&:after &:after
content "" content ""
display block display block

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="mkw-donation"> <div class="mkw-donation" :data-mobile="isMobile">
<article> <article>
<h1>%fa:heart%%i18n:desktop.tags.mk-donation-home-widget.title%</h1> <h1>%fa:heart%%i18n:desktop.tags.mk-donation-home-widget.title%</h1>
<p> <p>
@ -42,4 +42,17 @@ export default define({
font-size 0.8em font-size 0.8em
color #999 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> </style>

View file

@ -1,6 +1,10 @@
<template> <template>
<div class="mkw-nav"> <div class="mkw-nav">
<mk-widget-container>
<div :class="$style.body">
<mk-nav/> <mk-nav/>
</div>
</mk-widget-container>
</div> </div>
</template> </template>
@ -11,14 +15,12 @@ export default define({
}); });
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" module>
.mkw-nav .body
padding 16px padding 16px
font-size 12px font-size 12px
color #aaa color #aaa
background #fff background #fff
border solid 1px rgba(0, 0, 0, 0.075)
border-radius 6px
a a
color #999 color #999

View file

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

View file

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

View file

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

View file

@ -27,27 +27,19 @@ import friendsMaker from './friends-maker.vue';
import followers from './followers.vue'; import followers from './followers.vue';
import following from './following.vue'; import following from './following.vue';
import usersList from './users-list.vue'; import usersList from './users-list.vue';
import wNav from './widgets/nav.vue'; import widgetContainer from './widget-container.vue';
import wCalendar from './widgets/calendar.vue';
import wPhotoStream from './widgets/photo-stream.vue'; //#region widgets
import wSlideshow from './widgets/slideshow.vue';
import wTips from './widgets/tips.vue';
import wDonation from './widgets/donation.vue';
import wNotifications from './widgets/notifications.vue'; import wNotifications from './widgets/notifications.vue';
import wBroadcast from './widgets/broadcast.vue';
import wTimemachine from './widgets/timemachine.vue'; import wTimemachine from './widgets/timemachine.vue';
import wProfile from './widgets/profile.vue';
import wServer from './widgets/server.vue';
import wActivity from './widgets/activity.vue'; import wActivity from './widgets/activity.vue';
import wRss from './widgets/rss.vue';
import wTrends from './widgets/trends.vue'; import wTrends from './widgets/trends.vue';
import wVersion from './widgets/version.vue';
import wUsers from './widgets/users.vue'; import wUsers from './widgets/users.vue';
import wPolls from './widgets/polls.vue'; import wPolls from './widgets/polls.vue';
import wPostForm from './widgets/post-form.vue'; import wPostForm from './widgets/post-form.vue';
import wMessaging from './widgets/messaging.vue'; import wMessaging from './widgets/messaging.vue';
import wChannel from './widgets/channel.vue'; import wChannel from './widgets/channel.vue';
import wAccessLog from './widgets/access-log.vue'; //#endregion
Vue.component('mk-ui', ui); Vue.component('mk-ui', ui);
Vue.component('mk-ui-notification', uiNotification); Vue.component('mk-ui-notification', uiNotification);
@ -76,24 +68,16 @@ Vue.component('mk-friends-maker', friendsMaker);
Vue.component('mk-followers', followers); Vue.component('mk-followers', followers);
Vue.component('mk-following', following); Vue.component('mk-following', following);
Vue.component('mk-users-list', usersList); Vue.component('mk-users-list', usersList);
Vue.component('mkw-nav', wNav); Vue.component('mk-widget-container', widgetContainer);
Vue.component('mkw-calendar', wCalendar);
Vue.component('mkw-photo-stream', wPhotoStream); //#region widgets
Vue.component('mkw-slideshow', wSlideshow);
Vue.component('mkw-tips', wTips);
Vue.component('mkw-donation', wDonation);
Vue.component('mkw-notifications', wNotifications); Vue.component('mkw-notifications', wNotifications);
Vue.component('mkw-broadcast', wBroadcast);
Vue.component('mkw-timemachine', wTimemachine); Vue.component('mkw-timemachine', wTimemachine);
Vue.component('mkw-profile', wProfile);
Vue.component('mkw-server', wServer);
Vue.component('mkw-activity', wActivity); Vue.component('mkw-activity', wActivity);
Vue.component('mkw-rss', wRss);
Vue.component('mkw-trends', wTrends); Vue.component('mkw-trends', wTrends);
Vue.component('mkw-version', wVersion);
Vue.component('mkw-users', wUsers); Vue.component('mkw-users', wUsers);
Vue.component('mkw-polls', wPolls); Vue.component('mkw-polls', wPolls);
Vue.component('mkw-post-form', wPostForm); Vue.component('mkw-post-form', wPostForm);
Vue.component('mkw-messaging', wMessaging); Vue.component('mkw-messaging', wMessaging);
Vue.component('mkw-channel', wChannel); Vue.component('mkw-channel', wChannel);
Vue.component('mkw-access-log', wAccessLog); //#endregion

View file

@ -0,0 +1,72 @@
<template>
<div class="mk-widget-container" :class="{ naked }">
<header v-if="showHeader">
<div class="title"><slot name="header"></slot></div>
<slot name="func"></slot>
</header>
<slot></slot>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
showHeader: {
type: Boolean,
default: true
},
naked: {
type: Boolean,
default: false
}
}
});
</script>
<style lang="stylus" scoped>
.mk-widget-container
background #fff
border solid 1px rgba(0, 0, 0, 0.075)
border-radius 6px
overflow hidden
&.naked
background transparent !important
border none !important
> header
> .title
z-index 1
margin 0
padding 0 16px
line-height 42px
font-size 0.9em
font-weight bold
color #888
box-shadow 0 1px rgba(0, 0, 0, 0.07)
> [data-fa]
margin-right 4px
&:empty
display none
> button
position absolute
z-index 2
top 0
right 0
padding 0
width 42px
font-size 0.9em
line-height 42px
color #ccc
&:hover
color #aaa
&:active
color #999
</style>

View file

@ -1,122 +0,0 @@
<template>
<div class="mkw-photo-stream" :data-melt="props.design == 2">
<p class="title" v-if="props.design == 0">%fa:camera%%i18n:desktop.tags.mk-photo-stream-home-widget.title%</p>
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
<div class="stream" v-if="!fetching && images.length > 0">
<div v-for="image in images" :key="image.id" class="img" :style="`background-image: url(${image.url}?thumbnail&size=256)`"></div>
</div>
<p class="empty" v-if="!fetching && images.length == 0">%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p>
</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" scoped>
.mkw-photo-stream
background #fff
border solid 1px rgba(0, 0, 0, 0.075)
border-radius 6px
&[data-melt]
background transparent !important
border none !important
> .stream
padding 0
> .img
border solid 4px transparent
border-radius 8px
> .title
z-index 1
margin 0
padding 0 16px
line-height 42px
font-size 0.9em
font-weight bold
color #888
box-shadow 0 1px rgba(0, 0, 0, 0.07)
> [data-fa]
margin-right 4px
> .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>

View file

@ -1,111 +0,0 @@
<template>
<div class="mkw-rss">
<template v-if="!props.compact">
<p class="title">%fa:rss-square%RSS</p>
<button title="設定">%fa:cog%</button>
</template>
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
<div class="feed" v-else>
<a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a>
</div>
</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;
});
});
}
}
});
</script>
<style lang="stylus" scoped>
.mkw-rss
background #fff
border solid 1px rgba(0, 0, 0, 0.075)
border-radius 6px
> .title
margin 0
padding 0 16px
line-height 42px
font-size 0.9em
font-weight bold
color #888
box-shadow 0 1px rgba(0, 0, 0, 0.07)
> [data-fa]
margin-right 4px
> button
position absolute
top 0
right 0
padding 0
width 42px
font-size 0.9em
line-height 42px
color #ccc
&:hover
color #aaa
&:active
color #999
> .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
</style>

View file

@ -1,131 +0,0 @@
<template>
<div class="mkw-server" :data-melt="props.design == 2">
<template v-if="props.design == 0">
<p class="title">%fa:server%%i18n:desktop.tags.mk-server-home-widget.title%</p>
<button @click="toggle" title="%i18n:desktop.tags.mk-server-home-widget.toggle%">%fa:sort%</button>
</template>
<p class="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>
</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" scoped>
.mkw-server
background #fff
border solid 1px rgba(0, 0, 0, 0.075)
border-radius 6px
&[data-melt]
background transparent !important
border none !important
> .title
z-index 1
margin 0
padding 0 16px
line-height 42px
font-size 0.9em
font-weight bold
color #888
box-shadow 0 1px rgba(0, 0, 0, 0.07)
> [data-fa]
margin-right 4px
> button
position absolute
z-index 2
top 0
right 0
padding 0
width 42px
font-size 0.9em
line-height 42px
color #ccc
&:hover
color #aaa
&:active
color #999
> .fetching
margin 0
padding 16px
text-align center
color #aaa
> [data-fa]
margin-right 4px
</style>

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="root activity"> <div class="mk-activity">
<svg v-if="data" ref="canvas" viewBox="0 0 30 1" preserveAspectRatio="none"> <svg v-if="data" ref="canvas" viewBox="0 0 30 1" preserveAspectRatio="none">
<g v-for="(d, i) in data"> <g v-for="(d, i) in data">
<rect width="0.8" :height="d.postsH" <rect width="0.8" :height="d.postsH"
@ -47,7 +47,7 @@ export default Vue.extend({
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.root.activity .mk-activity
max-width 600px max-width 600px
margin 0 auto margin 0 auto

View file

@ -1,29 +0,0 @@
<template>
<div class="mk-home">
<mk-timeline @loaded="onTlLoaded"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
methods: {
onTlLoaded() {
this.$emit('loaded');
}
}
});
</script>
<style lang="stylus" scoped>
.mk-home
> .mk-timeline
max-width 600px
margin 0 auto
padding 8px
@media (min-width 500px)
padding 16px
</style>

View file

@ -1,7 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import ui from './ui.vue'; import ui from './ui.vue';
import home from './home.vue';
import timeline from './timeline.vue'; import timeline from './timeline.vue';
import posts from './posts.vue'; import posts from './posts.vue';
import imagesImage from './images-image.vue'; import imagesImage from './images-image.vue';
@ -19,9 +18,14 @@ import notificationPreview from './notification-preview.vue';
import usersList from './users-list.vue'; import usersList from './users-list.vue';
import userPreview from './user-preview.vue'; import userPreview from './user-preview.vue';
import userTimeline from './user-timeline.vue'; import userTimeline from './user-timeline.vue';
import activity from './activity.vue';
import widgetContainer from './widget-container.vue';
//#region widgets
import wActivity from './widgets/activity.vue';
//#endregion
Vue.component('mk-ui', ui); Vue.component('mk-ui', ui);
Vue.component('mk-home', home);
Vue.component('mk-timeline', timeline); Vue.component('mk-timeline', timeline);
Vue.component('mk-posts', posts); Vue.component('mk-posts', posts);
Vue.component('mk-images-image', imagesImage); Vue.component('mk-images-image', imagesImage);
@ -39,3 +43,9 @@ Vue.component('mk-notification-preview', notificationPreview);
Vue.component('mk-users-list', usersList); Vue.component('mk-users-list', usersList);
Vue.component('mk-user-preview', userPreview); Vue.component('mk-user-preview', userPreview);
Vue.component('mk-user-timeline', userTimeline); Vue.component('mk-user-timeline', userTimeline);
Vue.component('mk-activity', activity);
Vue.component('mk-widget-container', widgetContainer);
//#region widgets
Vue.component('mkw-activity', wActivity);
//#endregion

View file

@ -9,9 +9,7 @@
<h1> <h1>
<slot>Misskey</slot> <slot>Misskey</slot>
</h1> </h1>
<button v-if="func" @click="func"> <slot name="func"></slot>
<slot name="funcIcon"></slot>
</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="mk-ui"> <div class="mk-ui">
<x-header :func="func"> <x-header>
<template slot="funcIcon"><slot name="funcIcon"></slot></template> <template slot="func"><slot name="func"></slot></template>
<slot name="header"></slot> <slot name="header"></slot>
</x-header> </x-header>
<x-nav :is-open="isDrawerOpening"/> <x-nav :is-open="isDrawerOpening"/>
@ -23,7 +23,7 @@ export default Vue.extend({
XHeader, XHeader,
XNav XNav
}, },
props: ['title', 'func'], props: ['title'],
data() { data() {
return { return {
isDrawerOpening: false, isDrawerOpening: false,

View file

@ -0,0 +1,65 @@
<template>
<div class="mk-widget-container" :class="{ naked }">
<header v-if="showHeader">
<div class="title"><slot name="header"></slot></div>
<slot name="func"></slot>
</header>
<slot></slot>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
showHeader: {
type: Boolean,
default: true
},
naked: {
type: Boolean,
default: false
}
}
});
</script>
<style lang="stylus" scoped>
.mk-widget-container
background #eee
border-radius 8px
box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
overflow hidden
&.naked
background transparent !important
border none !important
> header
> .title
margin 0
padding 8px 10px
font-size 15px
font-weight normal
color #465258
background #fff
border-radius 8px 8px 0 0
> [data-fa]
margin-right 6px
&:empty
display none
> button
position absolute
z-index 2
top 0
right 0
padding 0
width 42px
height 100%
font-size 15px
color #465258
</style>

View file

@ -0,0 +1,23 @@
<template>
<div class="mkw-activity">
<mk-widget-container>
<template slot="header">%fa:chart-bar%アクティビティ</template>
<div :class="$style.body">
<mk-activity :user="os.i"/>
</div>
</mk-widget-container>
</div>
</template>
<script lang="ts">
import define from '../../../../common/define-widget';
export default define({
name: 'activity',
});
</script>
<style lang="stylus" module>
.body
padding 8px
</style>

View file

@ -1,11 +1,11 @@
<template> <template>
<mk-ui :func="fn"> <mk-ui>
<span slot="header"> <span slot="header">
<template v-if="folder">%fa:R folder-open%{{ folder.name }}</template> <template v-if="folder">%fa:R folder-open%{{ folder.name }}</template>
<template v-if="file"><mk-file-type-icon class="icon" :type="file.type"/>{{ file.name }}</template> <template v-if="file"><mk-file-type-icon class="icon" :type="file.type"/>{{ file.name }}</template>
<template v-if="!folder && !file">%fa:cloud%%i18n:mobile.tags.mk-drive-page.drive%</template> <template v-if="!folder && !file">%fa:cloud%%i18n:mobile.tags.mk-drive-page.drive%</template>
</span> </span>
<template slot="funcIcon">%fa:ellipsis-h%</template> <template slot="func"><button @click="fn">%fa:ellipsis-h%</button></template>
<mk-drive <mk-drive
ref="browser" ref="browser"
:init-folder="initFolder" :init-folder="initFolder"

View file

@ -1,24 +1,112 @@
<template> <template>
<mk-ui :func="fn"> <mk-ui>
<span slot="header">%fa:home%%i18n:mobile.tags.mk-home.home%</span> <span slot="header" @click="showTl = !showTl">
<template slot="funcIcon">%fa:pencil-alt%</template> <template v-if="showTl">%fa:home%タイムライン</template>
<mk-home @loaded="onHomeLoaded"/> <template v-else>%fa:home%ウィジェット</template>
<span style="margin-left:8px">
<template v-if="showTl">%fa:angle-down%</template>
<template v-else>%fa:angle-up%</template>
</span>
</span>
<template slot="func">
<button @click="fn" v-if="showTl">%fa:pencil-alt%</button>
<button @click="customizing = !customizing" v-else>%fa:cog%</button>
</template>
<main>
<div class="tl">
<mk-timeline @loaded="onLoaded" v-show="showTl"/>
</div>
<div class="widgets" v-if="!showTl">
<template v-if="customizing">
<header>
<select v-model="widgetAdderSelected">
<option value="profile">プロフィール</option>
<option value="calendar">カレンダー</option>
<option value="activity">アクティビティ</option>
<option value="rss">RSSリーダー</option>
<option value="photo-stream">フォトストリーム</option>
<option value="version">バージョン</option>
<option value="access-log">アクセスログ</option>
<option value="server">サーバー情報</option>
<option value="donation">寄付のお願い</option>
<option value="nav">ナビゲーション</option>
<option value="tips">ヒント</option>
</select>
<button @click="addWidget">追加</button>
<p>移動するにはをドラッグします削除するにはxをタップします</p>
</header>
<x-draggable
:list="widgets"
:options="{ handle: '.handle', animation: 150 }"
@sort="onWidgetSort"
>
<div v-for="widget in widgets" class="customize-container" :key="widget.id">
<header>
<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
</header>
<div>
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-mobile="true"/>
</div>
</div>
</x-draggable>
</template>
<template v-else>
<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/>
</template>
</div>
</main>
</mk-ui> </mk-ui>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import * as XDraggable from 'vuedraggable';
import * as uuid from 'uuid';
import Progress from '../../../common/scripts/loading'; import Progress from '../../../common/scripts/loading';
import getPostSummary from '../../../../../common/get-post-summary'; import getPostSummary from '../../../../../common/get-post-summary';
export default Vue.extend({ export default Vue.extend({
components: {
XDraggable
},
data() { data() {
return { return {
connection: null, connection: null,
connectionId: null, connectionId: null,
unreadCount: 0 unreadCount: 0,
showTl: true,
widgets: [],
customizing: false,
widgetAdderSelected: null
}; };
}, },
created() {
if ((this as any).os.i.client_settings.mobile_home == null) {
Vue.set((this as any).os.i.client_settings, 'mobile_home', [{
name: 'calendar',
id: 'a'
}, {
name: 'activity',
id: 'b'
}, {
name: 'rss',
id: 'c'
}, {
name: 'photo-stream',
id: 'd'
}, {
name: 'donation',
id: 'e'
}, {
name: 'nav',
id: 'f'
}, {
name: 'version',
id: 'g'
}]);
}
this.widgets = (this as any).os.i.client_settings.mobile_home;
},
mounted() { mounted() {
document.title = 'Misskey'; document.title = 'Misskey';
document.documentElement.style.background = '#313a42'; document.documentElement.style.background = '#313a42';
@ -40,7 +128,7 @@ export default Vue.extend({
fn() { fn() {
(this as any).apis.post(); (this as any).apis.post();
}, },
onHomeLoaded() { onLoaded() {
Progress.done(); Progress.done();
}, },
onStreamPost(post) { onStreamPost(post) {
@ -54,7 +142,81 @@ export default Vue.extend({
this.unreadCount = 0; this.unreadCount = 0;
document.title = 'Misskey'; document.title = 'Misskey';
} }
},
onWidgetSort() {
this.saveHome();
},
addWidget() {
const widget = {
name: this.widgetAdderSelected,
id: uuid(),
data: {}
};
this.widgets.unshift(widget);
this.saveHome();
},
removeWidget(widget) {
this.widgets = this.widgets.filter(w => w.id != widget.id);
this.saveHome();
},
saveHome() {
(this as any).api('i/update_mobile_home', {
home: this.widgets
});
},
warp() {
} }
} }
}); });
</script> </script>
<style lang="stylus" scoped>
main
> .tl
> .mk-timeline
max-width 600px
margin 0 auto
padding 8px
@media (min-width 500px)
padding 16px
> .widgets
margin 0 auto
max-width 500px
> header
padding 8px
background #fff
.widget
margin 8px
.customize-container
margin 8px
background #fff
> header
line-height 32px
background #eee
> .handle
padding 0 8px
> .remove
position absolute
top 0
right 0
padding 0 8px
line-height 32px
> div
padding 8px
> *
pointer-events none
</style>

View file

@ -1,7 +1,7 @@
<template> <template>
<mk-ui :func="fn"> <mk-ui>
<span slot="header">%fa:R bell%%i18n:mobile.tags.mk-notifications-page.notifications%</span> <span slot="header">%fa:R bell%%i18n:mobile.tags.mk-notifications-page.notifications%</span>
<span slot="funcIcon">%fa:check%</span> <template slot="func"><button @click="fn">%fa:check%</button></template>
<mk-notifications @fetched="onFetched"/> <mk-notifications @fetched="onFetched"/>
</mk-ui> </mk-ui>
</template> </template>

View file

@ -1,7 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header" v-if="!fetching">%fa:user% {{ user.name }}</span> <span slot="header" v-if="!fetching">%fa:user% {{ user.name }}</span>
<template slot="funcIcon">%fa:pencil-alt%</template>
<main v-if="!fetching"> <main v-if="!fetching">
<header> <header>
<div class="banner" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''"></div> <div class="banner" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''"></div>

View file

@ -16,7 +16,7 @@
<section class="activity"> <section class="activity">
<h2>%fa:chart-bar%%i18n:mobile.tags.mk-user-overview.activity%</h2> <h2>%fa:chart-bar%%i18n:mobile.tags.mk-user-overview.activity%</h2>
<div> <div>
<x-activity :user="user"/> <mk-activity :user="user"/>
</div> </div>
</section> </section>
<section class="frequently-replied-users"> <section class="frequently-replied-users">
@ -41,15 +41,13 @@ import XPosts from './home.posts.vue';
import XPhotos from './home.photos.vue'; import XPhotos from './home.photos.vue';
import XFriends from './home.friends.vue'; import XFriends from './home.friends.vue';
import XFollowersYouKnow from './home.followers-you-know.vue'; import XFollowersYouKnow from './home.followers-you-know.vue';
import XActivity from './home.activity.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XPosts, XPosts,
XPhotos, XPhotos,
XFriends, XFriends,
XFollowersYouKnow, XFollowersYouKnow
XActivity
}, },
props: ['user'] props: ['user']
}); });