diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2018-08-30 03:56:51 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-08-30 03:56:51 +0900 |
| commit | 4e11da98d90c1c44fce1abaf63c248896feff03a (patch) | |
| tree | cbe91363f87a3cc29b142433c16e4b16ccf2aa7d /src/client/app | |
| parent | New translations ja-JP.yml (French) (diff) | |
| parent | :art: (diff) | |
| download | misskey-4e11da98d90c1c44fce1abaf63c248896feff03a.tar.gz misskey-4e11da98d90c1c44fce1abaf63c248896feff03a.tar.bz2 misskey-4e11da98d90c1c44fce1abaf63c248896feff03a.zip | |
Merge branch 'develop' into l10n_develop
Diffstat (limited to 'src/client/app')
43 files changed, 984 insertions, 457 deletions
diff --git a/src/client/app/boot.js b/src/client/app/boot.js index 952881f6cb..54397c98c6 100644 --- a/src/client/app/boot.js +++ b/src/client/app/boot.js @@ -38,15 +38,22 @@ //#endregion //#region Detect the user language - let lang = navigator.language; + let lang = null; - if (!LANGS.includes(lang)) lang = lang.split('-')[0]; + if (LANGS.includes(navigator.language)) { + lang = navigator.language; + } else { + lang = LANGS.find(x => x.split('-')[0] == navigator.language); - // The default language is English - if (!LANGS.includes(lang)) lang = 'en'; + if (lang == null) { + // Fallback + lang = 'en-US'; + } + } - if (settings) { - if (settings.device.lang) lang = settings.device.lang; + if (settings && settings.device.lang && + LANGS.includes(settings.device.lang)) { + lang = settings.device.lang; } //#endregion diff --git a/src/client/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue index 1f745bf69d..27e66358e4 100644 --- a/src/client/app/common/views/components/nav.vue +++ b/src/client/app/common/views/components/nav.vue @@ -26,8 +26,8 @@ export default Vue.extend({ }, created() { (this as any).os.getMeta().then(meta => { - if (meta.repositoryUrl) this.repositoryUrl = meta.repositoryUrl; - if (meta.feedbackUrl) this.feedbackUrl = meta.feedbackUrl; + if (meta.maintainer.repository_url) this.repositoryUrl = meta.maintainer.repository_url; + if (meta.maintainer.feedback_url) this.feedbackUrl = meta.maintainer.feedback_url; }); } }); diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue index be69012737..242d9ba5c6 100644 --- a/src/client/app/common/views/components/url-preview.vue +++ b/src/client/app/common/views/components/url-preview.vue @@ -28,18 +28,99 @@ import Vue from 'vue'; import { url as misskeyUrl } from '../../../config'; +// THIS IS THE WHITELIST FOR THE EMBED PLAYER +const whiteList = [ + 'afreecatv.com', + 'aparat.com', + 'applemusic.com', + 'amazon.com', + 'awa.fm', + 'bandcamp.com', + 'bbc.co.uk', + 'beatport.com', + 'bilibili.com', + 'boomstream.com', + 'breakers.tv', + 'cam4.com', + 'cavelis.net', + 'chaturbate.com', + 'cnn.com', + 'cybergame.tv', + 'dailymotion.com', + 'deezer.com', + 'djlive.pl', + 'e-onkyo.com', + 'eventials.com', + 'facebook.com', + 'fc2.com', + 'gameplank.tv', + 'goodgame.ru', + 'google.com', + 'hardtunes.com', + 'instagram.com', + 'johnnylooch.com', + 'kexp.org', + 'lahzenegar.com', + 'liveedu.tv', + 'livetube.cc', + 'livestream.com', + 'meridix.com', + 'mixcloud.com', + 'mixer.com', + 'mobcrush.com', + 'mylive.in.th', + 'myspace.com', + 'netflix.com', + 'newretrowave.com', + 'nhk.or.jp', + 'nicovideo.jp', + 'nico.ms', + 'noisetrade.com', + 'nood.tv', + 'npr.org', + 'openrec.tv', + 'pandora.com', + 'pandora.tv', + 'picarto.tv', + 'pscp.tv', + 'restream.io', + 'reverbnation.com', + 'sermonaudio.com', + 'smashcast.tv', + 'songkick.com', + 'soundcloud.com', + 'spinninrecords.com', + 'spotify.com', + 'stitcher.com', + 'stream.me', + 'switchboard.live', + 'tunein.com', + 'twitcasting.tv', + 'twitch.tv', + 'twitter.com', + 'vaughnlive.tv', + 'veoh.com', + 'vimeo.com', + 'watchpeoplecode.com', + 'web.tv', + 'youtube.com', + 'youtu.be' +]; + export default Vue.extend({ props: { url: { type: String, require: true }, + detail: { type: Boolean, required: false, default: false } }, + data() { return { fetching: true, @@ -57,6 +138,7 @@ export default Vue.extend({ misskeyUrl }; }, + created() { const url = new URL(this.url); @@ -81,102 +163,27 @@ export default Vue.extend({ } return; } + fetch('/url?url=' + encodeURIComponent(this.url)).then(res => { res.json().then(info => { - if (info.url != null) { - this.title = info.title; - this.description = info.description; - this.thumbnail = info.thumbnail; - this.icon = info.icon; - this.sitename = info.sitename; - this.fetching = false; - if ([ // THIS IS THE WHITELIST FOR THE EMBED PLAYER - 'afreecatv.com', - 'aparat.com', - 'applemusic.com', - 'amazon.com', - 'awa.fm', - 'bandcamp.com', - 'bbc.co.uk', - 'beatport.com', - 'bilibili.com', - 'boomstream.com', - 'breakers.tv', - 'cam4.com', - 'cavelis.net', - 'chaturbate.com', - 'cnn.com', - 'cybergame.tv', - 'dailymotion.com', - 'deezer.com', - 'djlive.pl', - 'e-onkyo.com', - 'eventials.com', - 'facebook.com', - 'fc2.com', - 'gameplank.tv', - 'goodgame.ru', - 'google.com', - 'hardtunes.com', - 'instagram.com', - 'johnnylooch.com', - 'kexp.org', - 'lahzenegar.com', - 'liveedu.tv', - 'livetube.cc', - 'livestream.com', - 'meridix.com', - 'mixcloud.com', - 'mixer.com', - 'mobcrush.com', - 'mylive.in.th', - 'myspace.com', - 'netflix.com', - 'newretrowave.com', - 'nhk.or.jp', - 'nicovideo.jp', - 'nico.ms', - 'noisetrade.com', - 'nood.tv', - 'npr.org', - 'openrec.tv', - 'pandora.com', - 'pandora.tv', - 'picarto.tv', - 'pscp.tv', - 'restream.io', - 'reverbnation.com', - 'sermonaudio.com', - 'smashcast.tv', - 'songkick.com', - 'soundcloud.com', - 'spinninrecords.com', - 'spotify.com', - 'stitcher.com', - 'stream.me', - 'switchboard.live', - 'tunein.com', - 'twitcasting.tv', - 'twitch.tv', - 'twitter.com', - 'vaughnlive.tv', - 'veoh.com', - 'vimeo.com', - 'watchpeoplecode.com', - 'web.tv', - 'youtube.com', - 'youtu.be' - ].some(x => x == url.hostname || url.hostname.endsWith(`.${x}`))) - this.player = info.player; - } // info.url - }) // json - }); // fetch - } // created + if (info.url == null) return; + this.title = info.title; + this.description = info.description; + this.thumbnail = info.thumbnail; + this.icon = info.icon; + this.sitename = info.sitename; + this.fetching = false; + if (whiteList.some(x => x == url.hostname || url.hostname.endsWith(`.${x}`))) { + this.player = info.player; + } + }) + }); + } }); </script> <style lang="stylus" scoped> -.twitter +.player position relative width 100% diff --git a/src/client/app/common/views/filters/bytes.ts b/src/client/app/common/views/filters/bytes.ts index 3afb11e9ae..f7a1b2690f 100644 --- a/src/client/app/common/views/filters/bytes.ts +++ b/src/client/app/common/views/filters/bytes.ts @@ -1,8 +1,10 @@ import Vue from 'vue'; Vue.filter('bytes', (v, digits = 0) => { - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - if (v == 0) return '0Byte'; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + if (v == 0) return '0'; + const isMinus = v < 0; + if (isMinus) v = -v; const i = Math.floor(Math.log(v) / Math.log(1024)); - return (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; + return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; }); diff --git a/src/client/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue index 470576d5e6..544ca1bd9d 100644 --- a/src/client/app/common/views/widgets/donation.vue +++ b/src/client/app/common/views/widgets/donation.vue @@ -2,9 +2,9 @@ <div class="mkw-donation" :data-mobile="platform == 'mobile'"> <article> <h1>%fa:heart%%i18n:@title%</h1> - <p> + <p v-if="meta"> {{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }} - <a href="https://syuilo.com">@syuilo</a> + <a :href="meta.maintainer.url">{{ meta.maintainer.name }}</a> {{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }} </p> </article> @@ -15,6 +15,17 @@ import define from '../../../common/define-widget'; export default define({ name: 'donation' +}).extend({ + data() { + return { + meta: null + }; + }, + created() { + (this as any).os.getMeta().then(meta => { + this.meta = meta; + }); + } }); </script> diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts index 83820f92bd..e9d92d1eb1 100644 --- a/src/client/app/desktop/api/update-avatar.ts +++ b/src/client/app/desktop/api/update-avatar.ts @@ -3,8 +3,21 @@ import { apiUrl } from '../../config'; import CropWindow from '../views/components/crop-window.vue'; import ProgressDialog from '../views/components/progress-dialog.vue'; -export default (os: OS) => (cb, file = null) => { - const fileSelected = file => { +export default (os: OS) => { + + const cropImage = file => new Promise((resolve, reject) => { + + const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$'); + if (!regex.test(file.name) ) { + os.apis.dialog({ + title: '%fa:info-circle% %i18n:desktop.invalid-filetype%', + text: null, + actions: [{ + text: '%i18n:common.got-it%' + }] + }); + reject(); + } const w = os.new(CropWindow, { image: file, @@ -19,27 +32,29 @@ export default (os: OS) => (cb, file = null) => { os.api('drive/folders/find', { name: '%i18n:desktop.avatar%' - }).then(iconFolder => { - if (iconFolder.length === 0) { + }).then(avatarFolder => { + if (avatarFolder.length === 0) { os.api('drive/folders/create', { name: '%i18n:desktop.avatar%' }).then(iconFolder => { - upload(data, iconFolder); + resolve(upload(data, iconFolder)); }); } else { - upload(data, iconFolder[0]); + resolve(upload(data, avatarFolder[0])); } }); }); w.$once('skipped', () => { - set(file); + resolve(file); }); + w.$once('cancelled', reject); + document.body.appendChild(w.$el); - }; + }); - const upload = (data, folder) => { + const upload = (data, folder) => new Promise((resolve, reject) => { const dialog = os.new(ProgressDialog, { title: '%i18n:desktop.uploading-avatar%' }); @@ -52,18 +67,19 @@ export default (os: OS) => (cb, file = null) => { xhr.onload = e => { const file = JSON.parse((e.target as any).response); (dialog as any).close(); - set(file); + resolve(file); }; + xhr.onerror = reject; xhr.upload.onprogress = e => { if (e.lengthComputable) (dialog as any).update(e.loaded, e.total); }; xhr.send(data); - }; + }); - const set = file => { - os.api('i/update', { + const setAvatar = file => { + return os.api('i/update', { avatarId: file.id }).then(i => { os.store.commit('updateIKeyValue', { @@ -83,18 +99,21 @@ export default (os: OS) => (cb, file = null) => { }] }); - if (cb) cb(i); + return i; }); }; - if (file) { - fileSelected(file); - } else { - os.apis.chooseDriveFile({ - multiple: false, - title: '%fa:image% %i18n:desktop.choose-avatar%' - }).then(file => { - fileSelected(file); - }); - } + return (file = null) => { + const selectedFile = file + ? Promise.resolve(file) + : os.apis.chooseDriveFile({ + multiple: false, + title: '%fa:image% %i18n:desktop.choose-avatar%' + }); + + return selectedFile + .then(cropImage) + .then(setAvatar) + .catch(err => err && console.warn(err)); + }; }; diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts index 33c4e306a2..e8fa35149b 100644 --- a/src/client/app/desktop/api/update-banner.ts +++ b/src/client/app/desktop/api/update-banner.ts @@ -6,6 +6,19 @@ import ProgressDialog from '../views/components/progress-dialog.vue'; export default (os: OS) => { const cropImage = file => new Promise((resolve, reject) => { + + const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$'); + if (!regex.test(file.name) ) { + os.apis.dialog({ + title: '%fa:info-circle% %i18n:desktop.invalid-filetype%', + text: null, + actions: [{ + text: '%i18n:common.got-it%' + }] + }); + reject(); + } + const w = os.new(CropWindow, { image: file, title: '%i18n:desktop.banner-crop-title%', diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index 8dc0482191..f0e8a42662 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -25,6 +25,7 @@ import updateBanner from './api/update-banner'; import MkIndex from './views/pages/index.vue'; import MkDeck from './views/pages/deck/deck.vue'; import MkAdmin from './views/pages/admin/admin.vue'; +import MkStats from './views/pages/stats/stats.vue'; import MkUser from './views/pages/user/user.vue'; import MkFavorites from './views/pages/favorites.vue'; import MkSelectDrive from './views/pages/selectdrive.vue'; @@ -57,6 +58,7 @@ init(async (launch) => { { path: '/', name: 'index', component: MkIndex }, { path: '/deck', name: 'deck', component: MkDeck }, { path: '/admin', name: 'admin', component: MkAdmin }, + { path: '/stats', name: 'stats', component: MkStats }, { path: '/i/customize-home', component: MkHomeCustomize }, { path: '/i/favorites', component: MkFavorites }, { path: '/i/messaging/:user', component: MkMessagingRoom }, @@ -94,7 +96,7 @@ init(async (launch) => { /** * Init Notification */ - if ('Notification' in window) { + if ('Notification' in window && os.store.getters.isSignedIn) { // 許可を得ていなかったらリクエスト if ((Notification as any).permission == 'default') { await Notification.requestPermission(); diff --git a/src/client/app/desktop/views/components/charts.chart.ts b/src/client/app/desktop/views/components/charts.chart.ts new file mode 100644 index 0000000000..6a241631e9 --- /dev/null +++ b/src/client/app/desktop/views/components/charts.chart.ts @@ -0,0 +1,42 @@ +import Vue from 'vue'; +import { Line } from 'vue-chartjs'; +import * as mergeOptions from 'merge-options'; + +export default Vue.extend({ + extends: Line, + props: { + data: { + required: true + }, + opts: { + required: false + } + }, + watch: { + data() { + this.render(); + } + }, + mounted() { + this.render(); + }, + methods: { + render() { + this.renderChart(this.data, mergeOptions({ + responsive: true, + maintainAspectRatio: false, + scales: { + xAxes: [{ + type: 'time', + distribution: 'series' + }] + }, + tooltips: { + intersect: false, + mode: 'x', + position: 'nearest' + } + }, this.opts || {})); + } + } +}); diff --git a/src/client/app/desktop/views/components/charts.vue b/src/client/app/desktop/views/components/charts.vue new file mode 100644 index 0000000000..c4e92e429f --- /dev/null +++ b/src/client/app/desktop/views/components/charts.vue @@ -0,0 +1,587 @@ +<template> +<div class="gkgckalzgidaygcxnugepioremxvxvpt"> + <header> + <b>%i18n:@title%:</b> + <select v-model="chartType"> + <optgroup label="%i18n:@users%"> + <option value="users">%i18n:@charts.users%</option> + <option value="users-total">%i18n:@charts.users-total%</option> + </optgroup> + <optgroup label="%i18n:@notes%"> + <option value="notes">%i18n:@charts.notes%</option> + <option value="local-notes">%i18n:@charts.local-notes%</option> + <option value="remote-notes">%i18n:@charts.remote-notes%</option> + <option value="notes-total">%i18n:@charts.notes-total%</option> + </optgroup> + <optgroup label="%i18n:@drive%"> + <option value="drive-files">%i18n:@charts.drive-files%</option> + <option value="drive-files-total">%i18n:@charts.drive-files-total%</option> + <option value="drive">%i18n:@charts.drive%</option> + <option value="drive-total">%i18n:@charts.drive-total%</option> + </optgroup> + </select> + <div> + <span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span> + </div> + </header> + <div> + <x-chart v-if="chart" :data="data[0]" :opts="data[1]"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XChart from './charts.chart.ts'; + +const colors = { + local: 'rgb(246, 88, 79)', + remote: 'rgb(65, 221, 222)', + + localPlus: 'rgb(52, 178, 118)', + remotePlus: 'rgb(158, 255, 209)', + localMinus: 'rgb(255, 97, 74)', + remoteMinus: 'rgb(255, 149, 134)' +}; + +const rgba = (color: string): string => { + return color.replace('rgb', 'rgba').replace(')', ', 0.1)'); +}; + +export default Vue.extend({ + components: { + XChart + }, + + data() { + return { + chart: null, + chartType: 'notes', + span: 'hour' + }; + }, + + computed: { + data(): any { + if (this.chart == null) return null; + switch (this.chartType) { + case 'users': return this.usersChart(false); + case 'users-total': return this.usersChart(true); + case 'notes': return this.notesChart('combined'); + case 'local-notes': return this.notesChart('local'); + case 'remote-notes': return this.notesChart('remote'); + case 'notes-total': return this.notesTotalChart(); + case 'drive': return this.driveChart(); + case 'drive-total': return this.driveTotalChart(); + case 'drive-files': return this.driveFilesChart(); + case 'drive-files-total': return this.driveFilesTotalChart(); + } + }, + + stats(): any[] { + return ( + this.span == 'day' ? this.chart.perDay : + this.span == 'hour' ? this.chart.perHour : + null + ); + } + }, + + created() { + (this as any).api('chart', { + limit: 32 + }).then(chart => { + this.chart = chart; + }); + }, + + methods: { + notesChart(type: string): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + normal: type == 'local' ? x.notes.local.diffs.normal : type == 'remote' ? x.notes.remote.diffs.normal : x.notes.local.diffs.normal + x.notes.remote.diffs.normal, + reply: type == 'local' ? x.notes.local.diffs.reply : type == 'remote' ? x.notes.remote.diffs.reply : x.notes.local.diffs.reply + x.notes.remote.diffs.reply, + renote: type == 'local' ? x.notes.local.diffs.renote : type == 'remote' ? x.notes.remote.diffs.renote : x.notes.local.diffs.renote + x.notes.remote.diffs.renote, + all: type == 'local' ? (x.notes.local.inc + -x.notes.local.dec) : type == 'remote' ? (x.notes.remote.inc + -x.notes.remote.dec) : (x.notes.local.inc + -x.notes.local.dec) + (x.notes.remote.inc + -x.notes.remote.dec) + })); + + return [{ + datasets: [{ + label: 'All', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.all })) + }, { + label: 'Renotes', + fill: true, + backgroundColor: 'rgba(161, 222, 65, 0.1)', + borderColor: '#a1de41', + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.renote })) + }, { + label: 'Replies', + fill: true, + backgroundColor: 'rgba(247, 121, 108, 0.1)', + borderColor: '#f7796c', + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.reply })) + }, { + label: 'Normal', + fill: true, + backgroundColor: 'rgba(65, 221, 222, 0.1)', + borderColor: '#41ddde', + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.normal })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('number')(value); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; + } + } + } + }]; + }, + + notesTotalChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + localCount: x.notes.local.total, + remoteCount: x.notes.remote.total + })); + + return [{ + datasets: [{ + label: 'Combined', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteCount + x.localCount })) + }, { + label: 'Local', + fill: true, + backgroundColor: rgba(colors.local), + borderColor: colors.local, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localCount })) + }, { + label: 'Remote', + fill: true, + backgroundColor: rgba(colors.remote), + borderColor: colors.remote, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteCount })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('number')(value); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; + } + } + } + }]; + }, + + usersChart(total: boolean): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + localCount: total ? x.users.local.total : (x.users.local.inc + -x.users.local.dec), + remoteCount: total ? x.users.remote.total : (x.users.remote.inc + -x.users.remote.dec) + })); + + return [{ + datasets: [{ + label: 'Combined', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteCount + x.localCount })) + }, { + label: 'Local', + fill: true, + backgroundColor: rgba(colors.local), + borderColor: colors.local, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localCount })) + }, { + label: 'Remote', + fill: true, + backgroundColor: rgba(colors.remote), + borderColor: colors.remote, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteCount })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('number')(value); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; + } + } + } + }]; + }, + + driveChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + localInc: x.drive.local.incSize, + localDec: -x.drive.local.decSize, + remoteInc: x.drive.remote.incSize, + remoteDec: -x.drive.remote.decSize, + })); + + return [{ + datasets: [{ + label: 'All', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localInc + x.localDec + x.remoteInc + x.remoteDec })) + }, { + label: 'Local +', + fill: true, + backgroundColor: rgba(colors.localPlus), + borderColor: colors.localPlus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localInc })) + }, { + label: 'Local -', + fill: true, + backgroundColor: rgba(colors.localMinus), + borderColor: colors.localMinus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localDec })) + }, { + label: 'Remote +', + fill: true, + backgroundColor: rgba(colors.remotePlus), + borderColor: colors.remotePlus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteInc })) + }, { + label: 'Remote -', + fill: true, + backgroundColor: rgba(colors.remoteMinus), + borderColor: colors.remoteMinus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteDec })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('bytes')(value, 1); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`; + } + } + } + }]; + }, + + driveTotalChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + localSize: x.drive.local.totalSize, + remoteSize: x.drive.remote.totalSize + })); + + return [{ + datasets: [{ + label: 'Combined', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteSize + x.localSize })) + }, { + label: 'Local', + fill: true, + backgroundColor: rgba(colors.local), + borderColor: colors.local, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localSize })) + }, { + label: 'Remote', + fill: true, + backgroundColor: rgba(colors.remote), + borderColor: colors.remote, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteSize })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('bytes')(value, 1); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`; + } + } + } + }]; + }, + + driveFilesChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + localInc: x.drive.local.incCount, + localDec: -x.drive.local.decCount, + remoteInc: x.drive.remote.incCount, + remoteDec: -x.drive.remote.decCount + })); + + return [{ + datasets: [{ + label: 'All', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localInc + x.localDec + x.remoteInc + x.remoteDec })) + }, { + label: 'Local +', + fill: true, + backgroundColor: rgba(colors.localPlus), + borderColor: colors.localPlus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localInc })) + }, { + label: 'Local -', + fill: true, + backgroundColor: rgba(colors.localMinus), + borderColor: colors.localMinus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localDec })) + }, { + label: 'Remote +', + fill: true, + backgroundColor: rgba(colors.remotePlus), + borderColor: colors.remotePlus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteInc })) + }, { + label: 'Remote -', + fill: true, + backgroundColor: rgba(colors.remoteMinus), + borderColor: colors.remoteMinus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteDec })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('number')(value); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; + } + } + } + }]; + }, + + driveFilesTotalChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + localCount: x.drive.local.totalCount, + remoteCount: x.drive.remote.totalCount, + })); + + return [{ + datasets: [{ + label: 'Combined', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localCount + x.remoteCount })) + }, { + label: 'Local', + fill: true, + backgroundColor: rgba(colors.local), + borderColor: colors.local, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localCount })) + }, { + label: 'Remote', + fill: true, + backgroundColor: rgba(colors.remote), + borderColor: colors.remote, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteCount })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('number')(value); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; + } + } + } + }]; + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.gkgckalzgidaygcxnugepioremxvxvpt + padding 32px + background #fff + box-shadow 0 2px 8px rgba(#000, 0.1) + + * + user-select none + + > header + display flex + margin 0 0 1em 0 + padding 0 0 8px 0 + font-size 1em + color #555 + border-bottom solid 1px #eee + + > b + margin-right 8px + + > *:last-child + margin-left auto + + * + &:not(.active) + color $theme-color + cursor pointer + + > div + > * + display block + height 320px + +</style> diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue index 227bcc349d..1ba4a9a447 100644 --- a/src/client/app/desktop/views/components/note-detail.vue +++ b/src/client/app/desktop/views/components/note-detail.vue @@ -47,7 +47,7 @@ </div> <mk-poll v-if="p.poll" :note="p"/> <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> - <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> + <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <div class="map" v-if="p.geo" ref="map"></div> <div class="renote" v-if="p.renote"> <mk-note-preview :note="p.renote"/> diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue index 87acf7974d..7592ae3905 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -32,7 +32,7 @@ <mk-media-list :media-list="p.media"/> </div> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> - <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> + <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> <div class="map" v-if="p.geo" ref="map"></div> <div class="renote" v-if="p.renote"> <mk-note-preview :note="p.renote"/> diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index df131a1a65..7d6f1d55fb 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -49,6 +49,7 @@ </div> <mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/> <mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/> + <mk-switch v-model="$store.state.settings.showClockOnHeader" @change="onChangeShowClockOnHeader" text="%i18n:@show-clock-on-header%"/> <mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/> <mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/> <mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/> @@ -333,6 +334,12 @@ export default Vue.extend({ value: v }); }, + onChangeShowClockOnHeader(v) { + this.$store.dispatch('settings/set', { + key: 'showClockOnHeader', + value: v + }); + }, onChangeShowReplyTarget(v) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue index 4e0fc1cf1a..5e26389d89 100644 --- a/src/client/app/desktop/views/components/ui.header.account.vue +++ b/src/client/app/desktop/views/components/ui.header.account.vue @@ -30,10 +30,8 @@ <li @click="settings"> <p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p> </li> - </ul> - <ul> - <li @click="signout"> - <p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p> + <li v-if="$store.state.i.isAdmin"> + <router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link> </li> </ul> <ul> @@ -41,6 +39,11 @@ <p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p> </li> </ul> + <ul> + <li @click="signout"> + <p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p> + </li> + </ul> </div> </transition> </div> diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue index f01aade306..6292b764c6 100644 --- a/src/client/app/desktop/views/components/ui.header.nav.vue +++ b/src/client/app/desktop/views/components/ui.header.nav.vue @@ -11,7 +11,7 @@ <li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop"> <router-link to="/deck"> %fa:columns% - <p>%i18n:@deck% <small>(beta)</small></p> + <p>%i18n:@deck%</p> </router-link> </li> <li class="messaging"> diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue index edd9829c1c..6de4eaf744 100644 --- a/src/client/app/desktop/views/components/ui.header.vue +++ b/src/client/app/desktop/views/components/ui.header.vue @@ -17,7 +17,7 @@ <x-account v-if="$store.getters.isSignedIn"/> <x-notifications v-if="$store.getters.isSignedIn"/> <x-post v-if="$store.getters.isSignedIn"/> - <x-clock/> + <x-clock v-if="$store.state.settings.showClockOnHeader"/> </div> </div> </div> diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue index 7ef8dff5be..1e1755ec3c 100644 --- a/src/client/app/desktop/views/components/user-preview.vue +++ b/src/client/app/desktop/views/components/user-preview.vue @@ -48,7 +48,7 @@ export default Vue.extend({ this.open(); }); } else { - const query = this.user[0] == '@' ? + const query = this.user.startsWith('@') ? parseAcct(this.user.substr(1)) : { userId: this.user }; diff --git a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue index 3567585cb8..ebb54d782e 100644 --- a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue +++ b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue @@ -1,16 +1,20 @@ <template> -<div class="obdskegsannmntldydackcpzezagxqfy card"> +<div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card"> <header>%i18n:@dashboard%</header> <div v-if="stats" class="stats"> <div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div> <div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div> - <div><b>%fa:pen% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> - <div><span>%fa:pen% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> + <div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> + <div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> </div> <div class="cpu-memory"> <x-cpu-memory :connection="connection"/> </div> <div> + <label> + <input type="checkbox" v-model="disableRegistration" @change="updateMeta"> + <span>disableRegistration</span> + </label> <button class="ui" @click="invite">%i18n:@invite%</button> <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> </div> @@ -28,6 +32,7 @@ export default Vue.extend({ data() { return { stats: null, + disableRegistration: false, inviteCode: null, connection: null, connectionId: null @@ -37,6 +42,10 @@ export default Vue.extend({ this.connection = (this as any).os.streams.serverStatsStream.getConnection(); this.connectionId = (this as any).os.streams.serverStatsStream.use(); + (this as any).os.getMeta().then(meta => { + this.disableRegistration = meta.disableRegistration; + }); + (this as any).api('stats').then(stats => { this.stats = stats; }); @@ -49,6 +58,11 @@ export default Vue.extend({ (this as any).api('admin/invite').then(x => { this.inviteCode = x.code; }); + }, + updateMeta() { + (this as any).api('admin/update-meta', { + disableRegistration: this.disableRegistration + }); } } }); diff --git a/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue deleted file mode 100644 index 3c537d8d6d..0000000000 --- a/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue +++ /dev/null @@ -1,51 +0,0 @@ -<template> -<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> - <polyline - :points="points" - fill="none" - stroke-width="1" - stroke="#555"/> -</svg> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - chart: { - required: true - }, - type: { - type: String, - required: true - } - }, - data() { - return { - viewBoxX: 365, - viewBoxY: 70, - points: null - }; - }, - created() { - const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.drive.local.totalSize : d.drive.remote.totalSize)); - - if (peak != 0) { - const data = this.chart.slice().reverse().map(x => ({ - size: this.type == 'local' ? x.drive.local.totalSize : x.drive.remote.totalSize - })); - - this.points = data.map((d, i) => `${i},${(1 - (d.size / peak)) * this.viewBoxY}`).join(' '); - } - } -}); -</script> - -<style lang="stylus" scoped> -svg - display block - padding 10px - width 100% - -</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue b/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue deleted file mode 100644 index 4f94fd2372..0000000000 --- a/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue +++ /dev/null @@ -1,34 +0,0 @@ -<template> -<div class="card"> - <header>%i18n:@title%</header> - <div class="card"> - <header>%i18n:@local%</header> - <x-chart v-if="chart" :chart="chart" type="local"/> - </div> - <div class="card"> - <header>%i18n:@remote%</header> - <x-chart v-if="chart" :chart="chart" type="remote"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from "vue"; -import XChart from "./admin.drive-chart.chart.vue"; - -export default Vue.extend({ - components: { - XChart - }, - props: { - chart: { - required: true - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue deleted file mode 100644 index 83c61c1313..0000000000 --- a/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> - <polyline - :points="pointsNote" - fill="none" - stroke-width="1" - stroke="#41ddde"/> - <polyline - :points="pointsReply" - fill="none" - stroke-width="1" - stroke="#f7796c"/> - <polyline - :points="pointsRenote" - fill="none" - stroke-width="1" - stroke="#a1de41"/> - <polyline - :points="pointsTotal" - fill="none" - stroke-width="1" - stroke="#555" - stroke-dasharray="2 2"/> -</svg> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - chart: { - required: true - }, - type: { - type: String, - required: true - } - }, - data() { - return { - viewBoxX: 365, - viewBoxY: 70, - pointsNote: null, - pointsReply: null, - pointsRenote: null, - pointsTotal: null - }; - }, - created() { - const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.notes.local.diff : d.notes.remote.diff)); - - if (peak != 0) { - const data = this.chart.slice().reverse().map(x => ({ - normal: this.type == 'local' ? x.notes.local.diffs.normal : x.notes.remote.diffs.normal, - reply: this.type == 'local' ? x.notes.local.diffs.reply : x.notes.remote.diffs.reply, - renote: this.type == 'local' ? x.notes.local.diffs.renote : x.notes.remote.diffs.renote, - total: this.type == 'local' ? x.notes.local.diff : x.notes.remote.diff - })); - - this.pointsNote = data.map((d, i) => `${i},${(1 - (d.normal / peak)) * this.viewBoxY}`).join(' '); - this.pointsReply = data.map((d, i) => `${i},${(1 - (d.reply / peak)) * this.viewBoxY}`).join(' '); - this.pointsRenote = data.map((d, i) => `${i},${(1 - (d.renote / peak)) * this.viewBoxY}`).join(' '); - this.pointsTotal = data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' '); - } - } -}); -</script> - -<style lang="stylus" scoped> -svg - display block - padding 10px - width 100% - -</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue b/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue deleted file mode 100644 index e4d396d9c6..0000000000 --- a/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue +++ /dev/null @@ -1,34 +0,0 @@ -<template> -<div class="card"> - <header>%i18n:@title%</header> - <div class="card"> - <header>%i18n:@local%</header> - <x-chart v-if="chart" :chart="chart" type="local"/> - </div> - <div class="card"> - <header>%i18n:@remote%</header> - <x-chart v-if="chart" :chart="chart" type="remote"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from "vue"; -import XChart from "./admin.notes-chart.chart.vue"; - -export default Vue.extend({ - components: { - XChart - }, - props: { - chart: { - required: true - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue b/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue index 59932f4be7..8d8e37e181 100644 --- a/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue +++ b/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue @@ -1,5 +1,5 @@ <template> -<div class="card"> +<div class="mk-admin-card"> <header>%i18n:@suspend-user%</header> <input v-model="username" type="text" class="ui"/> <button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button> diff --git a/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue b/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue index a75c0bd64e..ec423969be 100644 --- a/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue +++ b/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue @@ -1,5 +1,5 @@ <template> -<div class="card"> +<div class="mk-admin-card"> <header>%i18n:@unsuspend-user%</header> <input v-model="username" type="text" class="ui"/> <button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button> diff --git a/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue b/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue index 72962870d9..e8204e69f4 100644 --- a/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue +++ b/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue @@ -1,5 +1,5 @@ <template> -<div class="card"> +<div class="mk-admin-card"> <header>%i18n:@unverify-user%</header> <input v-model="username" type="text" class="ui"/> <button class="ui" @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</button> diff --git a/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue deleted file mode 100644 index c2ab4a78e3..0000000000 --- a/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue +++ /dev/null @@ -1,51 +0,0 @@ -<template> -<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> - <polyline - :points="points" - fill="none" - stroke-width="1" - stroke="#555"/> -</svg> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - chart: { - required: true - }, - type: { - type: String, - required: true - } - }, - data() { - return { - viewBoxX: 365, - viewBoxY: 70, - points: null - }; - }, - created() { - const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.users.local.diff : d.users.remote.diff)); - - if (peak != 0) { - const data = this.chart.slice().reverse().map(x => ({ - count: this.type == 'local' ? x.users.local.diff : x.users.remote.diff - })); - - this.points = data.map((d, i) => `${i},${(1 - (d.count / peak)) * this.viewBoxY}`).join(' '); - } - } -}); -</script> - -<style lang="stylus" scoped> -svg - display block - padding 10px - width 100% - -</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.users-chart.vue b/src/client/app/desktop/views/pages/admin/admin.users-chart.vue deleted file mode 100644 index e620012702..0000000000 --- a/src/client/app/desktop/views/pages/admin/admin.users-chart.vue +++ /dev/null @@ -1,34 +0,0 @@ -<template> -<div class="card"> - <header>%i18n:@title%</header> - <div class="card"> - <header>%i18n:@local%</header> - <x-chart v-if="chart" :chart="chart" type="local"/> - </div> - <div class="card"> - <header>%i18n:@remote%</header> - <x-chart v-if="chart" :chart="chart" type="remote"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from "vue"; -import XChart from "./admin.users-chart.chart.vue"; - -export default Vue.extend({ - components: { - XChart - }, - props: { - chart: { - required: true - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.verify-user.vue b/src/client/app/desktop/views/pages/admin/admin.verify-user.vue index 3902d4bddd..91fb04af80 100644 --- a/src/client/app/desktop/views/pages/admin/admin.verify-user.vue +++ b/src/client/app/desktop/views/pages/admin/admin.verify-user.vue @@ -1,5 +1,5 @@ <template> -<div class="card"> +<div class="mk-admin-card"> <header>%i18n:@verify-user%</header> <input v-model="username" type="text" class="ui"/> <button class="ui" @click="verifyUser" :disabled="verifying">%i18n:@verify%</button> diff --git a/src/client/app/desktop/views/pages/admin/admin.vue b/src/client/app/desktop/views/pages/admin/admin.vue index cbb1890cc3..3438462cd6 100644 --- a/src/client/app/desktop/views/pages/admin/admin.vue +++ b/src/client/app/desktop/views/pages/admin/admin.vue @@ -11,9 +11,7 @@ <main> <div v-show="page == 'dashboard'"> <x-dashboard/> - <x-users-chart :chart="chart"/> - <x-notes-chart :chart="chart"/> - <x-drive-chart :chart="chart"/> + <x-charts/> </div> <div v-if="page == 'users'"> <x-suspend-user/> @@ -34,9 +32,7 @@ import XSuspendUser from "./admin.suspend-user.vue"; import XUnsuspendUser from "./admin.unsuspend-user.vue"; import XVerifyUser from "./admin.verify-user.vue"; import XUnverifyUser from "./admin.unverify-user.vue"; -import XUsersChart from "./admin.users-chart.vue"; -import XNotesChart from "./admin.notes-chart.vue"; -import XDriveChart from "./admin.drive-chart.vue"; +import XCharts from "../../components/charts.vue"; export default Vue.extend({ components: { @@ -45,21 +41,13 @@ export default Vue.extend({ XUnsuspendUser, XVerifyUser, XUnverifyUser, - XUsersChart, - XNotesChart, - XDriveChart + XCharts }, data() { return { - page: 'dashboard', - chart: null + page: 'dashboard' }; }, - created() { - (this as any).api('admin/chart').then(chart => { - this.chart = chart; - }); - }, methods: { nav(page: string) { this.page = page; @@ -115,7 +103,7 @@ export default Vue.extend({ > div max-width 800px -.card +.mk-admin-card padding 32px background #fff box-shadow 0 2px 8px rgba(#000, 0.1) diff --git a/src/client/app/desktop/views/pages/deck/deck.note.vue b/src/client/app/desktop/views/pages/deck/deck.note.vue index c7df715a05..e6d062eac9 100644 --- a/src/client/app/desktop/views/pages/deck/deck.note.vue +++ b/src/client/app/desktop/views/pages/deck/deck.note.vue @@ -32,7 +32,7 @@ <mk-media-list :media-list="p.media"/> </div> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> - <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> + <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <div class="renote" v-if="p.renote"> <mk-note-preview :note="p.renote" :mini="true"/> </div> diff --git a/src/client/app/desktop/views/pages/share.vue b/src/client/app/desktop/views/pages/share.vue index 4dd6080690..69ecbf115f 100644 --- a/src/client/app/desktop/views/pages/share.vue +++ b/src/client/app/desktop/views/pages/share.vue @@ -16,7 +16,7 @@ import Vue from 'vue'; export default Vue.extend({ data() { return { - name: (this as any).os.instanceName, + name: null, posted: false, text: new URLSearchParams(location.search).get('text') }; @@ -25,6 +25,11 @@ export default Vue.extend({ close() { window.close(); } + }, + mounted() { + (this as any).os.getMeta().then(meta => { + this.name = meta.name; + }); } }); </script> diff --git a/src/client/app/desktop/views/pages/stats/stats.vue b/src/client/app/desktop/views/pages/stats/stats.vue new file mode 100644 index 0000000000..41005b6398 --- /dev/null +++ b/src/client/app/desktop/views/pages/stats/stats.vue @@ -0,0 +1,64 @@ +<template> +<div class="tcrwdhwpuxrwmcttxjcsehgpagpstqey"> + <div v-if="stats" class="stats"> + <div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div> + <div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div> + <div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> + <div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> + </div> + <div> + <x-charts/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; +import XCharts from "../../components/charts.vue"; + +export default Vue.extend({ + components: { + XCharts + }, + data() { + return { + stats: null + }; + }, + created() { + (this as any).api('stats').then(stats => { + this.stats = stats; + }); + }, +}); +</script> + +<style lang="stylus"> +@import '~const.styl' + +.tcrwdhwpuxrwmcttxjcsehgpagpstqey + width 100% + padding 16px + + > .stats + display flex + justify-content center + margin-bottom 16px + padding 32px + background #fff + box-shadow 0 2px 8px rgba(#000, 0.1) + + > div + flex 1 + text-align center + + > *:first-child + display block + color $theme-color + + > *:last-child + font-size 70% + + > div + max-width 850px +</style> diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue index 4af0f0bca6..516eea0288 100644 --- a/src/client/app/desktop/views/pages/user/user.friends.vue +++ b/src/client/app/desktop/views/pages/user/user.friends.vue @@ -40,10 +40,12 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> +root(isDark) .friends - background #fff + background isDark ? #282C37 : #fff border solid 1px rgba(#000, 0.075) border-radius 6px + overflow hidden > .title z-index 1 @@ -52,7 +54,8 @@ export default Vue.extend({ line-height 42px font-size 0.9em font-weight bold - color #888 + background isDark ? #313543 : inherit + color isDark ? #e3e5e8 : #888 box-shadow 0 1px rgba(#000, 0.07) > i @@ -70,7 +73,7 @@ export default Vue.extend({ > .user padding 16px - border-bottom solid 1px #eee + border-bottom solid 1px isDark ? #21242f : #eee &:last-child border-bottom none @@ -96,18 +99,24 @@ export default Vue.extend({ margin 0 font-size 16px line-height 24px - color #555 + color isDark ? #ccc : #555 > .username display block margin 0 font-size 15px line-height 16px - color #ccc + color isDark ? #555 : #ccc > .mk-follow-button position absolute top 16px right 16px +.friends[data-darkmode] + root(true) + +.friends:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue index ce7791a96b..8397e56484 100644 --- a/src/client/app/desktop/views/pages/user/user.photos.vue +++ b/src/client/app/desktop/views/pages/user/user.photos.vue @@ -39,10 +39,12 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> +root(isDark) .photos - background #fff + background isDark ? #282C37 : #fff border solid 1px rgba(#000, 0.075) border-radius 6px + overflow hidden > .title z-index 1 @@ -51,7 +53,8 @@ export default Vue.extend({ line-height 42px font-size 0.9em font-weight bold - color #888 + background: isDark ? #313543 : inherit + color isDark ? #e3e5e8 : #888 box-shadow 0 1px rgba(#000, 0.07) > i @@ -85,4 +88,10 @@ export default Vue.extend({ > i margin-right 4px +.photos[data-darkmode] + root(true) + +.photos:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue index 300fd68f06..afb5e674d9 100644 --- a/src/client/app/desktop/views/pages/user/user.vue +++ b/src/client/app/desktop/views/pages/user/user.vue @@ -138,7 +138,7 @@ root(isDark) padding 16px font-size 12px color #aaa - background #fff + background isDark ? #21242f : #fff border solid 1px rgba(#000, 0.075) border-radius 6px diff --git a/src/client/app/init.ts b/src/client/app/init.ts index 18f510ea24..cf97957400 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -19,8 +19,8 @@ import { version, codename, lang } from './config'; let elementLocale; switch (lang) { - case 'ja': elementLocale = ElementLocaleJa; break; - case 'en': elementLocale = ElementLocaleEn; break; + case 'ja-JP': elementLocale = ElementLocaleJa; break; + case 'en-US': elementLocale = ElementLocaleEn; break; default: elementLocale = ElementLocaleEn; break; } diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue index 317f08dcfa..f9996f9da6 100644 --- a/src/client/app/mobile/views/components/note-detail.vue +++ b/src/client/app/mobile/views/components/note-detail.vue @@ -45,7 +45,7 @@ </div> <mk-poll v-if="p.poll" :note="p"/> <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> - <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> + <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <div class="map" v-if="p.geo" ref="map"></div> <div class="renote" v-if="p.renote"> <mk-note-preview :note="p.renote"/> diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue index 8fc8af7f8d..d0cea135f9 100644 --- a/src/client/app/mobile/views/components/note.vue +++ b/src/client/app/mobile/views/components/note.vue @@ -33,7 +33,7 @@ </div> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> <mk-url-preview v-for="url in urls" :url="url" :key="url"/> - <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> + <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <div class="map" v-if="p.geo" ref="map"></div> <div class="renote" v-if="p.renote"> <mk-note-preview :note="p.renote"/> diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue index 74564a48bb..39ea513b76 100644 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ b/src/client/app/mobile/views/components/ui.nav.vue @@ -30,6 +30,7 @@ <ul> <li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li> <li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li> + <li v-if="$store.getters.isSignedIn && $store.state.i.isAdmin"><router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link></li> <li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li> </ul> </div> diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index 6b82be099d..7437eb8b47 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -42,6 +42,12 @@ </ui-card> <ui-card> + <div slot="title">%fa:volume-up% %i18n:@sound%</div> + + <ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch> + </ui-card> + + <ui-card> <div slot="title">%fa:language% %i18n:@lang%</div> <ui-select v-model="lang" placeholder="%i18n:@auto%"> @@ -142,6 +148,11 @@ export default Vue.extend({ get() { return this.$store.state.device.lang; }, set(value) { this.$store.commit('device/set', { key: 'lang', value }); } }, + + enableSounds: { + get() { return this.$store.state.device.enableSounds; }, + set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); } + }, }, mounted() { diff --git a/src/client/app/mobile/views/pages/share.vue b/src/client/app/mobile/views/pages/share.vue index 588b0941e6..d75763c525 100644 --- a/src/client/app/mobile/views/pages/share.vue +++ b/src/client/app/mobile/views/pages/share.vue @@ -16,7 +16,7 @@ import Vue from 'vue'; export default Vue.extend({ data() { return { - name: (this as any).os.instanceName, + name: null, posted: false, text: new URLSearchParams(location.search).get('text') }; @@ -25,6 +25,11 @@ export default Vue.extend({ close() { window.close(); } + }, + mounted() { + (this as any).os.getMeta().then(meta => { + this.name = meta.name; + }); } }); </script> diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index e72867352f..8918847a8f 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -11,7 +11,7 @@ <a class="avatar"> <img :src="user.avatarUrl" alt="avatar"/> </a> - <mk-mute-button v-if="$store.state.i.id != user.id" :user="user"/> + <mk-mute-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/> <mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/> </div> <div class="title"> diff --git a/src/client/app/store.ts b/src/client/app/store.ts index ba91a11f25..469563495f 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -13,6 +13,7 @@ const defaultSettings = { showMaps: true, showPostFormOnTopOfTl: false, suggestRecentHashtags: true, + showClockOnHeader: true, circleIcons: true, gradientWindowHeader: false, showReplyTarget: true, |