Co-authored-by: MeiMei <30769358+mei23@users.noreply.github.com>
Co-authored-by: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com>
This commit is contained in:
syuilo
2020-01-30 04:37:25 +09:00
committed by GitHub
parent a5955c1123
commit f6154dc0af
871 changed files with 26140 additions and 71950 deletions

View File

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

View File

@ -0,0 +1,58 @@
import Vue from 'vue';
export default function <T extends object>(data: {
name: string;
props?: () => T;
}) {
return Vue.extend({
props: {
widget: {
type: Object
},
isCustomizeMode: {
type: Boolean,
default: false
}
},
computed: {
id(): string {
return this.widget.id;
},
props(): T {
return this.widget.data;
}
},
data() {
return {
bakedOldProps: null
};
},
created() {
this.mergeProps();
this.$watch('props', () => {
this.mergeProps();
});
},
methods: {
mergeProps() {
if (data.props) {
const defaultProps = data.props();
for (const prop of Object.keys(defaultProps)) {
if (this.props.hasOwnProperty(prop)) continue;
Vue.set(this.props, prop, defaultProps[prop]);
}
}
},
save() {
this.$store.dispatch('settings/updateWidget', this.widget);
}
}
});
}

View File

@ -0,0 +1,8 @@
import Vue from 'vue';
Vue.component('mkw-memo', () => import('./memo.vue').then(m => m.default));
Vue.component('mkw-notifications', () => import('./notifications.vue').then(m => m.default));
Vue.component('mkw-timeline', () => import('./timeline.vue').then(m => m.default));
Vue.component('mkw-calendar', () => import('./calendar.vue').then(m => m.default));
Vue.component('mkw-rss', () => import('./rss.vue').then(m => m.default));
Vue.component('mkw-trends', () => import('./trends.vue').then(m => m.default));

120
src/client/widgets/memo.vue Normal file
View File

@ -0,0 +1,120 @@
<template>
<div>
<mk-container :show-header="!props.compact">
<template #header><fa :icon="faStickyNote"/>{{ $t('_widgets.memo') }}</template>
<div class="otgbylcu">
<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea>
<button @click="saveMemo" :disabled="!changed">{{ $t('save') }}</button>
</div>
</mk-container>
</div>
</template>
<script lang="ts">
import { faStickyNote } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '../components/ui/container.vue';
import define from './define';
import i18n from '../i18n';
export default define({
name: 'memo',
props: () => ({
compact: false
})
}).extend({
i18n,
components: {
MkContainer
},
data() {
return {
text: null,
changed: false,
timeoutId: null,
faStickyNote
};
},
created() {
this.text = this.$store.state.settings.memo;
this.$watch('$store.state.settings.memo', text => {
this.text = text;
});
},
methods: {
func() {
this.props.compact = !this.props.compact;
this.save();
},
onChange() {
this.changed = true;
clearTimeout(this.timeoutId);
this.timeoutId = setTimeout(this.saveMemo, 1000);
},
saveMemo() {
this.$store.dispatch('settings/set', {
key: 'memo',
value: this.text
});
this.changed = false;
}
}
});
</script>
<style lang="scss" scoped>
.otgbylcu {
padding-bottom: 28px + 16px;
> textarea {
display: block;
width: 100%;
max-width: 100%;
min-width: 100%;
padding: 16px;
color: var(--inputText);
background: var(--face);
border: none;
border-bottom: solid var(--lineWidth) var(--faceDivider);
border-radius: 0;
}
> button {
display: block;
position: absolute;
bottom: 8px;
right: 8px;
margin: 0;
padding: 0 10px;
height: 28px;
color: #fff;
background: var(--accent) !important;
outline: none;
border: none;
border-radius: 4px;
transition: background 0.1s ease;
cursor: pointer;
&:hover {
background: var(--accentLighten10) !important;
}
&:active {
background: var(--accentDarken) !important;
transition: background 0s ease;
}
&:disabled {
opacity: 0.7;
cursor: default;
}
}
}
</style>

View File

@ -0,0 +1,46 @@
<template>
<div class="mkw-notifications">
<mk-container :show-header="!props.compact">
<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template>
<div style="height: 300px; overflow: auto; background: var(--bg);">
<x-notifications/>
</div>
</mk-container>
</div>
</template>
<script lang="ts">
import { faBell } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '../components/ui/container.vue';
import XNotifications from '../components/notifications.vue';
import define from './define';
import i18n from '../i18n';
export default define({
name: 'notifications',
props: () => ({
compact: false
})
}).extend({
i18n,
components: {
MkContainer,
XNotifications,
},
data() {
return {
faBell
};
},
methods: {
func() {
this.props.compact = !this.props.compact;
this.save();
},
}
});
</script>

101
src/client/widgets/rss.vue Normal file
View File

@ -0,0 +1,101 @@
<template>
<div>
<mk-container :show-header="!props.compact">
<template #header><fa :icon="faRssSquare"/>RSS</template>
<template #func><button class="_button" @click="setting"><fa :icon="faCog"/></button></template>
<div class="ekmkgxbj">
<mk-loading v-if="fetching"/>
<div class="feed" v-else>
<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
</div>
</div>
</mk-container>
</div>
</template>
<script lang="ts">
import { faRssSquare, faCog } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '../components/ui/container.vue';
import define from './define';
import i18n from '../i18n';
export default define({
name: 'rss',
props: () => ({
compact: false,
url: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews'
})
}).extend({
i18n,
components: {
MkContainer
},
data() {
return {
items: [],
fetching: true,
clock: null,
faRssSquare, faCog
};
},
mounted() {
this.fetch();
this.clock = setInterval(this.fetch, 60000);
},
beforeDestroy() {
clearInterval(this.clock);
},
methods: {
func() {
this.props.compact = !this.props.compact;
this.save();
},
fetch() {
fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {
}).then(res => {
res.json().then(feed => {
this.items = feed.items;
this.fetching = false;
});
});
},
setting() {
this.$root.dialog({
title: 'URL',
input: {
type: 'url',
default: this.props.url
}
}).then(({ canceled, result: url }) => {
if (canceled) return;
this.props.url = url;
this.save();
this.fetch();
});
}
}
});
</script>
<style lang="scss" scoped>
.ekmkgxbj {
> .feed {
padding: 0;
font-size: 0.9em;
> a {
display: block;
padding: 8px 16px;
color: var(--text);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&:nth-child(even) {
background: rgba(#000, 0.05);
}
}
}
}
</style>

View File

@ -0,0 +1,113 @@
<template>
<div class="mkw-timeline">
<mk-container :show-header="!props.compact">
<template #header>
<button @click="choose" class="_button">
<fa v-if="props.src === 'home'" :icon="faHome"/>
<fa v-if="props.src === 'local'" :icon="faComments"/>
<fa v-if="props.src === 'social'" :icon="faShareAlt"/>
<fa v-if="props.src === 'global'" :icon="faGlobe"/>
<fa v-if="props.src === 'list'" :icon="faListUl"/>
<fa v-if="props.src === 'antenna'" :icon="faSatellite"/>
<span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span>
<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
</button>
</template>
<div style="height: 300px; padding: 8px; overflow: auto; background: var(--bg);">
<x-timeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list" :antenna="props.antenna"/>
</div>
</mk-container>
</div>
</template>
<script lang="ts">
import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite } from '@fortawesome/free-solid-svg-icons';
import { faComments } from '@fortawesome/free-regular-svg-icons';
import MkContainer from '../components/ui/container.vue';
import XTimeline from '../components/timeline.vue';
import define from './define';
import i18n from '../i18n';
export default define({
name: 'timeline',
props: () => ({
src: 'home',
list: null,
compact: false
})
}).extend({
i18n,
components: {
MkContainer,
XTimeline,
},
data() {
return {
menuOpened: false,
faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite
};
},
methods: {
func() {
this.props.compact = !this.props.compact;
this.save();
},
async choose(ev) {
this.menuOpened = true;
const [antennas, lists] = await Promise.all([
this.$root.api('antennas/list'),
this.$root.api('users/lists/list')
]);
const antennaItems = antennas.map(antenna => ({
text: antenna.name,
icon: faSatellite,
action: () => {
this.props.antenna = antenna;
this.setSrc('antenna');
}
}));
const listItems = lists.map(list => ({
text: list.name,
icon: faListUl,
action: () => {
this.props.list = list;
this.setSrc('list');
}
}));
this.$root.menu({
items: [{
text: this.$t('_timelines.home'),
icon: faHome,
action: () => { this.setSrc('home') }
}, {
text: this.$t('_timelines.local'),
icon: faComments,
action: () => { this.setSrc('local') }
}, {
text: this.$t('_timelines.social'),
icon: faShareAlt,
action: () => { this.setSrc('social') }
}, {
text: this.$t('_timelines.global'),
icon: faGlobe,
action: () => { this.setSrc('global') }
}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems],
noCenter: true,
source: ev.currentTarget || ev.target
}).then(() => {
this.menuOpened = false;
});
},
setSrc(src) {
this.props.src = src;
this.save();
},
}
});
</script>

View File

@ -0,0 +1,89 @@
<template>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" style="overflow:visible">
<defs>
<linearGradient :id="gradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
</linearGradient>
<mask :id="maskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
<polygon
:points="polygonPoints"
fill="#fff"
fill-opacity="0.5"/>
<polyline
:points="polylinePoints"
fill="none"
stroke="#fff"
stroke-width="2"/>
<circle
:cx="headX"
:cy="headY"
r="3"
fill="#fff"/>
</mask>
</defs>
<rect
x="-10" y="-10"
:width="viewBoxX + 20" :height="viewBoxY + 20"
:style="`stroke: none; fill: url(#${ gradientId }); mask: url(#${ maskId })`"/>
</svg>
</template>
<script lang="ts">
import Vue from 'vue';
import { v4 as uuid } from 'uuid';
export default Vue.extend({
props: {
src: {
type: Array,
required: true
}
},
data() {
return {
viewBoxX: 50,
viewBoxY: 30,
gradientId: uuid(),
maskId: uuid(),
polylinePoints: '',
polygonPoints: '',
headX: null,
headY: null,
clock: null
};
},
watch: {
src() {
this.draw();
}
},
created() {
this.draw();
// Vueが何故かWatchを発動させない場合があるので
this.clock = setInterval(this.draw, 1000);
},
beforeDestroy() {
clearInterval(this.clock);
},
methods: {
draw() {
const stats = this.src.slice().reverse();
const peak = Math.max.apply(null, stats) || 1;
const polylinePoints = stats.map((n, i) => [
i * (this.viewBoxX / (stats.length - 1)),
(1 - (n / peak)) * this.viewBoxY
]);
this.polylinePoints = polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.polygonPoints = `0,${ this.viewBoxY } ${ this.polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.headX = polylinePoints[polylinePoints.length - 1][0];
this.headY = polylinePoints[polylinePoints.length - 1][1];
}
}
});
</script>

View File

@ -0,0 +1,124 @@
<template>
<div>
<mk-container :show-header="!props.compact">
<template #header><fa :icon="faHashtag"/>{{ $t('_widgets.trends') }}</template>
<div class="wbrkwala">
<transition-group tag="div" name="chart">
<div v-for="stat in stats" :key="stat.tag">
<div class="tag">
<router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
<p>{{ $t('count').replace('{}', stat.usersCount) }}</p>
</div>
<x-chart class="chart" :src="stat.chart"/>
</div>
</transition-group>
</div>
</mk-container>
</div>
</template>
<script lang="ts">
import { faHashtag } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '../components/ui/container.vue';
import define from './define';
import i18n from '../i18n';
import XChart from './trends.chart.vue';
export default define({
name: 'hashtags',
props: () => ({
compact: false
})
}).extend({
i18n,
components: {
MkContainer, XChart
},
data() {
return {
stats: [],
fetching: true,
faHashtag
};
},
mounted() {
this.fetch();
this.clock = setInterval(this.fetch, 1000 * 60);
},
beforeDestroy() {
clearInterval(this.clock);
},
methods: {
func() {
this.props.compact = !this.props.compact;
this.save();
},
fetch() {
this.$root.api('hashtags/trend').then(stats => {
this.stats = stats;
this.fetching = false;
});
}
}
});
</script>
<style lang="scss" scoped>
.wbrkwala {
> .fetching,
> .empty {
margin: 0;
padding: 16px;
text-align: center;
color: var(--text);
opacity: 0.7;
> [data-icon] {
margin-right: 4px;
}
}
> div {
.chart-move {
transition: transform 1s ease;
}
> div {
display: flex;
align-items: center;
padding: 14px 16px;
&:not(:last-child) {
border-bottom: solid 1px var(--divider);
}
> .tag {
flex: 1;
overflow: hidden;
font-size: 14px;
color: var(--fg);
> a {
display: block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: inherit;
}
> p {
margin: 0;
font-size: 75%;
opacity: 0.7;
}
}
> .chart {
height: 30px;
}
}
}
}
</style>